-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Allow skipping a missing custom change (Copy of 6627) #7390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Allow skipping a missing custom change (Copy of 6627) #7390
Conversation
Prior to this a custom change with a class name that is not on the classpath would always fail the entire changelog. Now `changeSet.failOnError: false` will be honoured if the cause of the failure is the failure to instantiate the change class. In addition, a failing precondition for the changeset with onFail="MARK_RAN" will be honoured if the class is not on the classpath. Fixes #6520 Signed-off-by: Robert Elliot <rob@lidalia.org.uk>
Also fixes failing test by loading the change in generateChecksum Signed-off-by: Robert Elliot <rob@lidalia.org.uk>
Signed-off-by: Robert Elliot <rob@lidalia.org.uk>
|
|
|
To be closed when #6627 is merged. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
WalkthroughAdds two integration tests and changelog fixtures for missing custom-change classes, implements lazy loading and safer error handling in CustomChangeWrapper, and skips a network test on macOS. Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
liquibase-integration-tests/src/test/java/liquibase/dbtest/AbstractIntegrationTest.java(2 hunks)liquibase-integration-tests/src/test/resources/changelogs/common/missingcustomchange/missing_custom_change_fail_on_error_false.changelog.xml(1 hunks)liquibase-integration-tests/src/test/resources/changelogs/common/missingcustomchange/missing_custom_change_precondition_failed.changelog.xml(1 hunks)liquibase-standard/src/main/java/liquibase/change/custom/CustomChangeWrapper.java(7 hunks)liquibase-standard/src/test/groovy/liquibase/util/NetUtilTest.groovy(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
liquibase-standard/src/main/java/liquibase/change/custom/CustomChangeWrapper.java (3)
liquibase-standard/src/main/java/liquibase/Scope.java (1)
Scope(45-591)liquibase-standard/src/main/java/liquibase/util/OsgiUtil.java (1)
OsgiUtil(8-67)liquibase-standard/src/main/java/liquibase/util/ObjectUtil.java (1)
ObjectUtil(35-539)
liquibase-integration-tests/src/test/java/liquibase/dbtest/AbstractIntegrationTest.java (1)
liquibase-standard/src/main/java/liquibase/snapshot/SnapshotGeneratorFactory.java (1)
SnapshotGeneratorFactory(25-484)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
- GitHub Check: Build & Package
- GitHub Check: Run Test for (Java 25 macos-13)
- GitHub Check: Run Test for (Java 21 ubuntu-22.04)
- GitHub Check: Run Test for (Java 25 ubuntu-22.04)
- GitHub Check: Run Test for (Java 21 macos-13)
- GitHub Check: Run Test for (Java 21 windows-latest)
- GitHub Check: Run Test for (Java 25 windows-latest)
- GitHub Check: Run Test for (Java 17 ubuntu-22.04)
- GitHub Check: Analyze (java)
liquibase-standard/src/main/java/liquibase/change/custom/CustomChangeWrapper.java
Outdated
Show resolved
Hide resolved
Prior to this a custom change with a class name that is not on the classpath would always fail the entire changelog. Now `changeSet.failOnError: false` will be honoured if the cause of the failure is the failure to instantiate the change class. In addition, a failing precondition for the changeset with onFail="MARK_RAN" will be honoured if the class is not on the classpath. Fixes #6520 Signed-off-by: Robert Elliot <rob@lidalia.org.uk>
Also fixes failing test by loading the change in generateChecksum Signed-off-by: Robert Elliot <rob@lidalia.org.uk>
Signed-off-by: Robert Elliot <rob@lidalia.org.uk>
Pull Request Review: Allow skipping a missing custom changeSummaryThis PR implements lazy loading for custom change classes to allow graceful handling when a custom change class is missing. The changes enable scenarios where:
Code Quality & Best Practices✅ Strengths
|
- CustomChangeWrapper validation error/warning added to keep consistency with other scenarios instead of throwing an exceptionto keep consistency with other scenarios. - Integration tests fixed.
…ttps://github.com/liquibase/liquibase into Mahoney-forks-allow-skipping-missing-custom-change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!configured) { | ||
| try { | ||
| configureCustomChange(); | ||
| } catch (CustomChangeException e) { | ||
| throw new UnexpectedLiquibaseException(e); | ||
| if (this.customChange == null) { | ||
| try { | ||
| this.customChange = loadCustomChange(className); | ||
| } catch (CustomChangeException e) { | ||
| return new ValidationErrors().addWarning("Exception thrown loading " + getClassName() + ": " + e.getMessage()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| configureCustomChange(); |
Copilot
AI
Nov 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic only loads the custom change when !configured is true, but then always calls configureCustomChange(). This can fail if the custom change is null and the configured flag is already true from a previous call. The condition on line 140 should be removed, or the null check should be moved outside the !configured block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
liquibase-integration-tests/src/test/java/liquibase/dbtest/AbstractIntegrationTest.java (1)
1432-1437: Consider escaping the table name for better database compatibility.The SQL query construction directly concatenates the table name without using
database.escapeTableName(). For consistency with other tests in this file (see lines 479-484 and 509-514), consider escaping the table name to ensure compatibility across all database types.Apply this diff to improve database compatibility:
- ResultSet rs = stmt.executeQuery( - "SELECT ID, EXECTYPE FROM " + database.getDatabaseChangeLogTableName() + - " WHERE ID = 'missing_custom_change_precondition_failed' AND EXECTYPE = 'MARK_RAN'" - ); + String tableName = database.escapeTableName( + database.getLiquibaseCatalogName(), + database.getLiquibaseSchemaName(), + database.getDatabaseChangeLogTableName() + ); + ResultSet rs = stmt.executeQuery( + "SELECT ID, EXECTYPE FROM " + tableName + + " WHERE ID = 'missing_custom_change_precondition_failed' AND EXECTYPE = 'MARK_RAN'" + );liquibase-standard/src/main/java/liquibase/change/custom/CustomChangeWrapper.java (2)
74-80: Consider removing unused exception declaration.The method signature declares
throws CustomChangeExceptionbut no longer throws it after moving to lazy loading. While maintaining the signature preserves compatibility, the unused exception may confuse callers about when to expect it.
238-261: Consider simplifying nested structure for readability.The nested if-else blocks (lines 247-260) correctly handle all cases but could be flattened for improved readability.
Apply this diff to simplify the control flow:
@Override public CheckSum generateCheckSum() { if (this.customChange == null) { try { this.customChange = loadCustomChange(className); } catch (CustomChangeException e) { final Logger log = Scope.getCurrentScope().getLog(getClass()); log.warning("Exception thrown loading " + getClassName() + ", not using its generateChecksum", e); - } - } - if (customChange != null) { - try { - configureCustomChange(); - if (customChange instanceof CustomChangeChecksum) { - return ((CustomChangeChecksum) customChange).generateChecksum(); - } else { - return super.generateCheckSum(); - } - } catch (CustomChangeException e) { - throw new UnexpectedLiquibaseException(e); - } - } else { - return super.generateCheckSum(); + return super.generateCheckSum(); + } } + + if (!(customChange instanceof CustomChangeChecksum)) { + return super.generateCheckSum(); + } + + try { + configureCustomChange(); + return ((CustomChangeChecksum) customChange).generateChecksum(); + } catch (CustomChangeException e) { + throw new UnexpectedLiquibaseException(e); + } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
liquibase-integration-tests/src/test/java/liquibase/dbtest/AbstractIntegrationTest.java(2 hunks)liquibase-standard/src/main/java/liquibase/change/custom/CustomChangeWrapper.java(7 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
liquibase-standard/src/main/java/liquibase/change/custom/CustomChangeWrapper.java (3)
liquibase-standard/src/main/java/liquibase/Scope.java (1)
Scope(45-591)liquibase-standard/src/main/java/liquibase/util/OsgiUtil.java (1)
OsgiUtil(8-67)liquibase-standard/src/main/java/liquibase/util/ObjectUtil.java (1)
ObjectUtil(35-539)
liquibase-integration-tests/src/test/java/liquibase/dbtest/AbstractIntegrationTest.java (1)
liquibase-standard/src/main/java/liquibase/snapshot/SnapshotGeneratorFactory.java (1)
SnapshotGeneratorFactory(25-484)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
- GitHub Check: Run Test for (Java 17 ubuntu-22.04)
- GitHub Check: Run Test for (Java 25 windows-latest)
- GitHub Check: Run Test for (Java 21 ubuntu-22.04)
- GitHub Check: Run Test for (Java 25 macos-13)
- GitHub Check: Run Test for (Java 21 windows-latest)
- GitHub Check: Run Test for (Java 21 macos-13)
- GitHub Check: Run Test for (Java 25 ubuntu-22.04)
- GitHub Check: Build & Package
- GitHub Check: CodeQL analysis (java)
- GitHub Check: claude-review / claude-review
- GitHub Check: Analyze (java)
🔇 Additional comments (8)
liquibase-integration-tests/src/test/java/liquibase/dbtest/AbstractIntegrationTest.java (2)
64-64: LGTM!The
ResultSetimport is necessary for the new test that directly queries the DATABASECHANGELOG table.
1374-1405: LGTM!The test correctly verifies that a missing custom change class with
failOnError="false"doesn't throwClassNotFoundException. The logic is sound, including the SQLite special handling and the verification that the DATABASECHANGELOG table was created.liquibase-standard/src/main/java/liquibase/change/custom/CustomChangeWrapper.java (6)
7-7: LGTM!The Logger import supports the new warning-level logging in failure paths, which aligns with the graceful degradation approach.
60-69: Past review concern successfully addressed.The method now maintains source compatibility by wrapping
CustomChangeExceptioninUnexpectedLiquibaseExceptionwhile implementing the lazy-loading behavior. The signature remains unchanged from the original public API.
82-101: Excellent consolidation of loading logic.Extracting the class-loading logic into a single private method follows the DRY principle and makes the lazy-loading pattern easier to maintain across multiple call sites.
287-307: LGTM!The lazy-loading guard (lines 292-294) ensures
customChangeis initialized before attempting to configure properties, preventing potential null-pointer issues.
378-394: Excellent defensive approach in metadata loading.Using a local variable (line 378) to attempt exploratory loading for property detection, with graceful degradation on failure (lines 386-393), prevents parse-time crashes while still enabling dynamic property discovery when the custom change class is available.
140-163: Consider caching failed configuration attempts to avoid repeatedconfigureCustomChange()calls on validation retry.The concern about repeated configuration attempts is valid. When
validate()is called multiple times andconfigureCustomChange()throws an exception before line 303, theconfiguredflag remainsfalse. On subsequentvalidate()calls, lines 151 will unconditionally callconfigureCustomChange()again, which will retry the property setting andsetUp()call (lines 296–301), even though they failed before.While the code is safe due to the guard at line 288 in
configureCustomChange(), repeated failed configuration calls are inefficient. If this validation is called frequently during debugging or processing, consider:
- Caching the failure state separately from the
configuredflag- Recording whether a configuration attempt failed to avoid retry
- Setting
configured = trueearlier to prevent re-entry, even on partial failures
Pull Request ReviewSummaryThis PR successfully implements lazy loading for custom change classes, allowing changesets with missing custom change classes to be skipped when Code Quality and Architecture ✅Strengths:
Minor Concerns:
Potential Bugs and Issues
|
Impact
Description
Things to be aware of
Things to worry about
Additional Context
Summary by CodeRabbit
Bug Fixes
Refactor
Tests