diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..6142595
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,275 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+max_line_length = 120
+ij_formatter_off_tag = @formatter:off
+ij_formatter_on_tag = @formatter:on
+ij_formatter_tags_enabled = false
+ij_smart_tabs = false
+ij_wrap_on_typing = false
+
+[*.yml]
+indent_style = space
+indent_size = 2
+
+[*.{md,xml,sh,json,gradle,html}]
+indent_style = space
+indent_size = 4
+
+[*.java]
+indent_style = tab
+tab_width = 4
+ij_continuation_indent_size = 4
+### import rules
+# explicitly disable imports on demand
+ij_java_packages_to_use_import_on_demand = false
+ij_java_imports_layout = $*,|,java.**,|,javax.**,|,org.**,|,com.**,|,*
+ij_java_class_count_to_use_import_on_demand = 99
+ij_java_names_count_to_use_import_on_demand = 1
+ij_java_layout_static_imports_separately = true
+ij_java_use_single_class_imports = true
+###
+ij_java_align_consecutive_assignments = false
+ij_java_align_consecutive_variable_declarations = false
+ij_java_align_group_field_declarations = false
+ij_java_align_multiline_annotation_parameters = false
+ij_java_align_multiline_array_initializer_expression = false
+ij_java_align_multiline_assignment = false
+ij_java_align_multiline_binary_operation = true
+ij_java_align_multiline_chained_methods = false
+ij_java_align_multiline_extends_list = false
+ij_java_align_multiline_for = true
+ij_java_align_multiline_method_parentheses = false
+ij_java_align_multiline_parameters = true
+ij_java_align_multiline_parameters_in_calls = false
+ij_java_align_multiline_parenthesized_expression = false
+ij_java_align_multiline_records = true
+ij_java_align_multiline_resources = true
+ij_java_align_multiline_ternary_operation = false
+ij_java_align_multiline_text_blocks = false
+ij_java_align_multiline_throws_list = false
+ij_java_align_subsequent_simple_methods = false
+ij_java_align_throws_keyword = false
+ij_java_annotation_parameter_wrap = off
+ij_java_array_initializer_new_line_after_left_brace = false
+ij_java_array_initializer_right_brace_on_new_line = false
+ij_java_array_initializer_wrap = off
+ij_java_assert_statement_colon_on_next_line = false
+ij_java_assert_statement_wrap = off
+ij_java_assignment_wrap = off
+ij_java_binary_operation_sign_on_next_line = false
+ij_java_binary_operation_wrap = off
+ij_java_blank_lines_after_anonymous_class_header = 0
+ij_java_blank_lines_after_class_header = 0
+ij_java_blank_lines_after_imports = 1
+ij_java_blank_lines_after_package = 1
+ij_java_blank_lines_around_class = 1
+ij_java_blank_lines_around_field = 0
+ij_java_blank_lines_around_field_in_interface = 0
+ij_java_blank_lines_around_initializer = 1
+ij_java_blank_lines_around_method = 1
+ij_java_blank_lines_around_method_in_interface = 1
+ij_java_blank_lines_before_class_end = 0
+ij_java_blank_lines_before_imports = 1
+ij_java_blank_lines_before_method_body = 0
+ij_java_blank_lines_before_package = 0
+ij_java_block_brace_style = end_of_line
+ij_java_block_comment_at_first_column = true
+ij_java_call_parameters_new_line_after_left_paren = false
+ij_java_call_parameters_right_paren_on_new_line = false
+ij_java_call_parameters_wrap = off
+ij_java_case_statement_on_separate_line = true
+ij_java_catch_on_new_line = false
+ij_java_class_annotation_wrap = split_into_lines
+ij_java_class_brace_style = end_of_line
+ij_java_class_names_in_javadoc = 1
+ij_java_do_not_indent_top_level_class_members = false
+ij_java_do_not_wrap_after_single_annotation = false
+ij_java_do_while_brace_force = never
+ij_java_doc_add_blank_line_after_description = true
+ij_java_doc_add_blank_line_after_param_comments = false
+ij_java_doc_add_blank_line_after_return = false
+ij_java_doc_add_p_tag_on_empty_lines = true
+ij_java_doc_align_exception_comments = true
+ij_java_doc_align_param_comments = true
+ij_java_doc_do_not_wrap_if_one_line = false
+ij_java_doc_enable_formatting = true
+ij_java_doc_enable_leading_asterisks = true
+ij_java_doc_indent_on_continuation = false
+ij_java_doc_keep_empty_lines = true
+ij_java_doc_keep_empty_parameter_tag = true
+ij_java_doc_keep_empty_return_tag = true
+ij_java_doc_keep_empty_throws_tag = true
+ij_java_doc_keep_invalid_tags = true
+ij_java_doc_param_description_on_new_line = false
+ij_java_doc_preserve_line_breaks = false
+ij_java_doc_use_throws_not_exception_tag = true
+ij_java_else_on_new_line = false
+ij_java_entity_dd_suffix = EJB
+ij_java_entity_eb_suffix = Bean
+ij_java_entity_hi_suffix = Home
+ij_java_entity_lhi_prefix = Local
+ij_java_entity_lhi_suffix = Home
+ij_java_entity_li_prefix = Local
+ij_java_entity_pk_class = java.lang.String
+ij_java_entity_vo_suffix = VO
+ij_java_enum_constants_wrap = off
+ij_java_extends_keyword_wrap = off
+ij_java_extends_list_wrap = off
+ij_java_field_annotation_wrap = split_into_lines
+ij_java_finally_on_new_line = false
+ij_java_for_brace_force = never
+ij_java_for_statement_new_line_after_left_paren = false
+ij_java_for_statement_right_paren_on_new_line = false
+ij_java_for_statement_wrap = off
+ij_java_generate_final_locals = false
+ij_java_generate_final_parameters = false
+ij_java_if_brace_force = never
+ij_java_indent_case_from_switch = true
+ij_java_insert_inner_class_imports = false
+ij_java_insert_override_annotation = true
+ij_java_keep_blank_lines_before_right_brace = 2
+ij_java_keep_blank_lines_between_package_declaration_and_header = 2
+ij_java_keep_blank_lines_in_code = 2
+ij_java_keep_blank_lines_in_declarations = 2
+ij_java_keep_control_statement_in_one_line = true
+ij_java_keep_first_column_comment = true
+ij_java_keep_indents_on_empty_lines = false
+ij_java_keep_line_breaks = true
+ij_java_keep_multiple_expressions_in_one_line = false
+ij_java_keep_simple_blocks_in_one_line = false
+ij_java_keep_simple_classes_in_one_line = false
+ij_java_keep_simple_lambdas_in_one_line = false
+ij_java_keep_simple_methods_in_one_line = false
+ij_java_label_indent_absolute = false
+ij_java_label_indent_size = 0
+ij_java_lambda_brace_style = end_of_line
+ij_java_line_comment_add_space = false
+ij_java_line_comment_at_first_column = true
+ij_java_message_dd_suffix = EJB
+ij_java_message_eb_suffix = Bean
+ij_java_method_annotation_wrap = split_into_lines
+ij_java_method_brace_style = end_of_line
+ij_java_method_call_chain_wrap = off
+ij_java_method_parameters_new_line_after_left_paren = false
+ij_java_method_parameters_right_paren_on_new_line = false
+ij_java_method_parameters_wrap = off
+ij_java_modifier_list_wrap = false
+ij_java_new_line_after_lparen_in_record_header = false
+ij_java_parameter_annotation_wrap = off
+ij_java_parentheses_expression_new_line_after_left_paren = false
+ij_java_parentheses_expression_right_paren_on_new_line = false
+ij_java_place_assignment_sign_on_next_line = false
+ij_java_prefer_longer_names = true
+ij_java_prefer_parameters_wrap = false
+ij_java_record_components_wrap = normal
+ij_java_repeat_synchronized = true
+ij_java_replace_instanceof_and_cast = false
+ij_java_replace_null_check = true
+ij_java_replace_sum_lambda_with_method_ref = true
+ij_java_resource_list_new_line_after_left_paren = false
+ij_java_resource_list_right_paren_on_new_line = false
+ij_java_resource_list_wrap = off
+ij_java_rparen_on_new_line_in_record_header = false
+ij_java_session_dd_suffix = EJB
+ij_java_session_eb_suffix = Bean
+ij_java_session_hi_suffix = Home
+ij_java_session_lhi_prefix = Local
+ij_java_session_lhi_suffix = Home
+ij_java_session_li_prefix = Local
+ij_java_session_si_suffix = Service
+ij_java_space_after_closing_angle_bracket_in_type_argument = false
+ij_java_space_after_colon = true
+ij_java_space_after_comma = true
+ij_java_space_after_comma_in_type_arguments = true
+ij_java_space_after_for_semicolon = true
+ij_java_space_after_quest = true
+ij_java_space_after_type_cast = true
+ij_java_space_before_annotation_array_initializer_left_brace = false
+ij_java_space_before_annotation_parameter_list = false
+ij_java_space_before_array_initializer_left_brace = true
+ij_java_space_before_catch_keyword = true
+ij_java_space_before_catch_left_brace = true
+ij_java_space_before_catch_parentheses = true
+ij_java_space_before_class_left_brace = true
+ij_java_space_before_colon = true
+ij_java_space_before_colon_in_foreach = true
+ij_java_space_before_comma = false
+ij_java_space_before_do_left_brace = true
+ij_java_space_before_else_keyword = true
+ij_java_space_before_else_left_brace = true
+ij_java_space_before_finally_keyword = true
+ij_java_space_before_finally_left_brace = true
+ij_java_space_before_for_left_brace = true
+ij_java_space_before_for_parentheses = true
+ij_java_space_before_for_semicolon = false
+ij_java_space_before_if_left_brace = true
+ij_java_space_before_if_parentheses = true
+ij_java_space_before_method_call_parentheses = false
+ij_java_space_before_method_left_brace = true
+ij_java_space_before_method_parentheses = false
+ij_java_space_before_opening_angle_bracket_in_type_parameter = false
+ij_java_space_before_quest = true
+ij_java_space_before_switch_left_brace = true
+ij_java_space_before_switch_parentheses = true
+ij_java_space_before_synchronized_left_brace = true
+ij_java_space_before_synchronized_parentheses = true
+ij_java_space_before_try_left_brace = true
+ij_java_space_before_try_parentheses = true
+ij_java_space_before_type_parameter_list = false
+ij_java_space_before_while_keyword = true
+ij_java_space_before_while_left_brace = true
+ij_java_space_before_while_parentheses = true
+ij_java_space_inside_one_line_enum_braces = false
+ij_java_space_within_empty_array_initializer_braces = false
+ij_java_space_within_empty_method_call_parentheses = false
+ij_java_space_within_empty_method_parentheses = false
+ij_java_spaces_around_additive_operators = true
+ij_java_spaces_around_assignment_operators = true
+ij_java_spaces_around_bitwise_operators = true
+ij_java_spaces_around_equality_operators = true
+ij_java_spaces_around_lambda_arrow = true
+ij_java_spaces_around_logical_operators = true
+ij_java_spaces_around_method_ref_dbl_colon = false
+ij_java_spaces_around_multiplicative_operators = true
+ij_java_spaces_around_relational_operators = true
+ij_java_spaces_around_shift_operators = true
+ij_java_spaces_around_type_bounds_in_type_parameters = true
+ij_java_spaces_around_unary_operator = false
+ij_java_spaces_within_angle_brackets = false
+ij_java_spaces_within_annotation_parentheses = false
+ij_java_spaces_within_array_initializer_braces = true
+ij_java_spaces_within_braces = false
+ij_java_spaces_within_brackets = false
+ij_java_spaces_within_cast_parentheses = false
+ij_java_spaces_within_catch_parentheses = false
+ij_java_spaces_within_for_parentheses = false
+ij_java_spaces_within_if_parentheses = false
+ij_java_spaces_within_method_call_parentheses = false
+ij_java_spaces_within_method_parentheses = false
+ij_java_spaces_within_parentheses = false
+ij_java_spaces_within_switch_parentheses = false
+ij_java_spaces_within_synchronized_parentheses = false
+ij_java_spaces_within_try_parentheses = false
+ij_java_spaces_within_while_parentheses = false
+ij_java_special_else_if_treatment = true
+ij_java_subclass_name_suffix = Impl
+ij_java_ternary_operation_signs_on_next_line = false
+ij_java_ternary_operation_wrap = off
+ij_java_test_name_suffix = Test
+ij_java_throws_keyword_wrap = off
+ij_java_throws_list_wrap = off
+ij_java_use_external_annotations = false
+ij_java_use_fq_class_names = false
+ij_java_use_relative_indents = false
+ij_java_variable_annotation_wrap = off
+ij_java_visibility = public
+ij_java_while_brace_force = never
+ij_java_while_on_new_line = false
+ij_java_wrap_comments = false
+ij_java_wrap_first_method_in_call_chain = false
+ij_java_wrap_long_lines = false
\ No newline at end of file
diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml
new file mode 100644
index 0000000..4b31496
--- /dev/null
+++ b/.github/workflows/gradle-wrapper-validation.yml
@@ -0,0 +1,15 @@
+name: "Validate Gradle Wrapper"
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ validation:
+ name: "Validation"
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: gradle/wrapper-validation-action@v1.0.4
diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
new file mode 100644
index 0000000..6136046
--- /dev/null
+++ b/.github/workflows/gradle.yml
@@ -0,0 +1,47 @@
+name: CI
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ java: [ '17', '21', '22' ]
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Java
+ uses: actions/setup-java@v1
+ with:
+ java-version: ${{ matrix.java }}
+ - name: Build with Gradle
+ run: ./gradlew build
+ - name: Archive test reports
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: Gradle Test Reports Java ${{ matrix.java }}
+ path: build/reports/tests/test
+
+
+ publishCoverage:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Java
+ uses: actions/setup-java@v1
+ with:
+ java-version: 21
+ - name: Build with Gradle
+ run: ./gradlew jacocoTestReport
+ - name: Upload coverage reports to Codecov
+ uses: codecov/codecov-action@v4.0.1
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: ./build/reports/jacoco/test/jacocoTestReport.xml
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d6a9a6c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+.idea/
+
+build/
+out/
+
+.gradle/
+
+**/data/test/output/
+**/data/test/tmp/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..85e90e0
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2016 cronn GmbH
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..88764bb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,117 @@
+[![CI](https://github.com/cronn/liquibase-changelog-generator/workflows/CI/badge.svg)](https://github.com/cronn/liquibase-changelog-generator/actions)
+[![Maven Central](https://maven-badges.herokuapp.com/maven-central/de.cronn/liquibase-changelog-generator/badge.svg)](http://maven-badges.herokuapp.com/maven-central/de.cronn/liquibase-changelog-generator)
+[![Apache 2.0](https://img.shields.io/github/license/cronn/liquibase-changelog-generator.svg)](http://www.apache.org/licenses/LICENSE-2.0)
+[![codecov](https://codecov.io/gh/cronn/liquibase-changelog-generator/branch/main/graph/badge.svg?token=KD1WJK5ZFK)](https://codecov.io/gh/cronn/liquibase-changelog-generator)
+[![Valid Gradle Wrapper](https://github.com/cronn/liquibase-changelog-generator/workflows/Validate%20Gradle%20Wrapper/badge.svg)](https://github.com/cronn/liquibase-changelog-generator/actions/workflows/gradle-wrapper-validation.yml)
+
+# Liquibase Changelog Generator #
+
+The `liquibase-changelog-generator` library implements an auto-generation of Liquibase changelogs
+based on the Hibernate metamodel. The library was designed to be used in a JUnit test.
+
+## Usage
+
+Depending on the database, you need to add the following Maven **test** dependency to your project:
+
+### PostgreSQL
+
+```xml
+
+ de.cronn
+ liquibase-changelog-generator-postgresql
+ 1.0
+ test
+
+```
+
+## Overview
+
+This library provides a mechanism to implement a unit test that sets up two databases using [Testcontainers](testcontainers):
+
+1. **Hibernate Database**: Uses the database schema populated by Hibernate, based on the annotations of the `jakarta.persistence.Entity` classes.
+2. **Liquibase Database**: Uses the Liquibase changelog to populate the database schema.
+
+Once both databases are ready, we use the [DiffToChangeLog](liquibase-diff) mechanism of Liquibase to compare the two databases.
+If the diff is non-empty, the test fails and it outputs the required Liquibase changes.
+This ensures that the build pipeline can only succeed if there are no missing changesets.
+We recommend to assert that diff using our [validation-file-assertions] library.
+
+### Example
+
+```java
+class LiquibaseTest implements JUnit5ValidationFileAssertions {
+ @Test
+ void testLiquibaseAndHibernatePopulationsAreConsistent() {
+ HibernateToLiquibaseDiff hibernateToLiquibaseDiff = new HibernateToLiquibaseDiffForPostgres("My Author");
+ String diff = hibernateToLiquibaseDiff.generateDiff(HibernatePopulatedConfig.class, LiquibasePopulatedConfig.class);
+ assertWithFile(diff, FileExtensions.XML);
+ }
+
+ @EntityScan("de.cronn.example")
+ static class HibernatePopulatedConfig extends HibernatePopulatedConfigForPostgres {
+ }
+
+ @PropertySource("classpath:/liquibase-test-liquibase.properties")
+ static class LiquibasePopulatedConfig extends LiquibasePopulatedConfigForPostgres {
+ }
+}
+```
+
+Then define the path to the Liquibase changelog via `src/test/resources/liquibase-test-liquibase.properties`
+
+```properties
+spring.liquibase.change-log=classpath:/migrations/changelog.xml
+```
+
+## Steps to Change/Extend the Database Schema
+
+As a developer, you typically follow these steps when you want to change or extend the database schema:
+
+1. **Modify Entity Classes**: Create, modify, or delete a `@Entity` class as desired.
+2. **Run the Test**: Execute the `LiquibaseTest`. The test will fail and output the generated Liquibase changeset in the form of a difference to the validation file.
+3. **Update Changelog**: Take the generated Liquibase changeset and add it to the Liquibase changelog file(s).
+4. **Review Changeset**: ⚠ Review the auto-generated changeset very carefully! Consider it as only a **suggestion** or a **template**.
+ The Liquibase diff mechanism is not perfect. For instance, when renaming a column, it will yield a drop-column and a create-column statement.
+ In such cases, you need to adjust the changeset manually.
+5. **Re-run the Test**: Re-run the `LiquibaseTest` and check that the test now succeeds.
+
+## Additional Notes
+
+The test performs some automatic post-processing of the diff. For example, it overrides the Hibernate-generated
+primary-key and foreign-key names. See the `HibernateToLiquibaseDiff.filterDiffResult(DiffResult)` method for details.
+
+## Hibernate Schema Export
+
+We also provide a utility class to export the schema that Hibernate would create.
+This can be useful as a first feedback during a (major) change of the JPA entities.
+
+### Example
+
+```java
+class HibernateSchemaTest implements JUnit5ValidationFileAssertions {
+ @Test
+ void testExport() {
+ String schema = new HibernateSchemaExport(HibernatePopulatedConfig.class).export();
+ assertWithFile(schema, FileExtensions.SQL);
+ }
+
+ @EntityScan("de.cronn.example")
+ static class HibernatePopulatedConfig extends HibernatePopulatedConfigForPostgres {
+ }
+}
+```
+
+## Requirements ##
+
+- Java 17+
+- Spring Boot 3.3.1+
+- Liquibase 4.27.0+
+- Hibernate 6.5.2+
+
+## Related Projects ##
+
+- [https://github.com/liquibase/liquibase-hibernate](https://github.com/liquibase/liquibase-hibernate)
+
+[testcontainers]: https://testcontainers.com/
+[liquibase-diff]: https://docs.liquibase.com/commands/inspection/diff-changelog.html
+[validation-file-assertions]: https://github.com/cronn/validation-file-assertions
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..1b3d5f1
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,186 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencyLocking {
+ lockAllConfigurations()
+ }
+}
+
+plugins {
+ id "java-test-fixtures"
+ id 'org.springframework.boot' version 'latest.release' apply false
+ id 'io.spring.dependency-management' version 'latest.release' apply false
+}
+
+allprojects {
+ apply plugin: 'java-library'
+ apply plugin: 'jacoco'
+ apply plugin: 'io.spring.dependency-management'
+ apply plugin: 'org.springframework.boot'
+
+ group = 'de.cronn'
+ version = '1.0-SNAPSHOT'
+
+ repositories {
+ mavenCentral()
+ }
+
+ bootJar {
+ enabled = false
+ }
+
+ bootRun {
+ enabled = false
+ }
+
+ java {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ tasks.withType(JavaCompile) {
+ options.encoding = 'UTF-8'
+ options.compilerArgs.addAll(['-Werror'])
+ }
+
+ dependencies {
+ implementation 'org.springframework:spring-context'
+ implementation 'org.springframework.boot:spring-boot-autoconfigure'
+
+ testImplementation 'org.junit.jupiter:junit-jupiter-params'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+
+ testRuntimeOnly 'ch.qos.logback:logback-classic'
+
+ components.all { ComponentMetadataDetails details ->
+ if (details.id.version =~ /(?i).+([-.])(CANDIDATE|RC|BETA|ALPHA|CR\d+|M\d+).*/) {
+ details.status = 'milestone'
+ }
+ }
+
+ dependencyLocking {
+ lockAllConfigurations()
+ }
+ }
+
+ test {
+ useJUnitPlatform()
+
+ maxHeapSize = "256m"
+
+ inputs.dir('data/test/validation')
+ outputs.dir('data/test/output')
+ outputs.dir('data/test/tmp')
+ }
+}
+
+dependencies {
+ implementation 'org.slf4j:slf4j-api'
+ implementation 'org.slf4j:jul-to-slf4j'
+
+ api 'org.liquibase:liquibase-core'
+ implementation 'org.testcontainers:jdbc'
+
+ implementation 'org.hibernate.orm:hibernate-core'
+
+ testFixturesApi 'org.assertj:assertj-core'
+ testFixturesApi 'org.springframework.boot:spring-boot-starter-data-jpa'
+ testFixturesApi 'de.cronn:test-utils:latest.release'
+ testFixturesApi 'de.cronn:validation-file-assertions:latest.release'
+}
+
+// Disable publication of test fixtures as documented on https://docs.gradle.org/8.8/userguide/java_testing.html#sec:java_test_fixtures
+components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() }
+components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() }
+
+wrapper {
+ gradleVersion = "8.9"
+ distributionType = Wrapper.DistributionType.ALL
+}
+
+jacocoTestReport {
+ reports {
+ xml.required = true
+ }
+ dependsOn test
+}
+
+allprojects {
+ apply plugin: 'maven-publish'
+ apply plugin: 'signing'
+
+ task sourcesJar(type: Jar, dependsOn: classes) {
+ archiveClassifier = 'sources'
+ from sourceSets.main.allSource
+ }
+
+ task javadocJar(type: Jar, dependsOn: javadoc) {
+ archiveClassifier = 'javadoc'
+ from javadoc.destinationDir
+ }
+
+ publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ groupId = project.group
+ artifactId = project.name
+ version = project.version
+ pom {
+ name = project.name
+ description = 'Liquibase Changelog Generator'
+ url = 'https://github.com/cronn/liquibase-changelog-generator'
+
+ licenses {
+ license {
+ name = "The Apache Software License, Version 2.0"
+ url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
+ distribution = "repo"
+ }
+ }
+
+ developers {
+ developer {
+ id = "benedikt.waldvogel"
+ name = "Benedikt Waldvogel"
+ email = "benedikt.waldvogel@cronn.de"
+ }
+ }
+
+ scm {
+ url = "https://github.com/cronn/liquibase-changelog-generator"
+ }
+ }
+
+ from components.java
+
+ artifact sourcesJar
+ artifact javadocJar
+
+ versionMapping {
+ usage('java-api') {
+ fromResolutionOf('runtimeClasspath')
+ }
+ usage('java-runtime') {
+ fromResolutionResult()
+ }
+ }
+ }
+ }
+ repositories {
+ maven {
+ url "https://oss.sonatype.org/service/local/staging/deploy/maven2"
+ credentials {
+ username = project.hasProperty('nexusUsername') ? project.property('nexusUsername') : System.getenv('NEXUS_USERNAME')
+ password = project.hasProperty('nexusPassword') ? project.property('nexusPassword') : System.getenv('NEXUS_PASSWORD')
+ }
+ }
+ }
+ }
+
+ signing {
+ useGpgCmd()
+ sign publishing.publications.mavenJava
+ }
+}
diff --git a/buildscript-gradle.lockfile b/buildscript-gradle.lockfile
new file mode 100644
index 0000000..11fcd1d
--- /dev/null
+++ b/buildscript-gradle.lockfile
@@ -0,0 +1,27 @@
+# This is a Gradle generated file for dependency locking.
+# Manual edits can break the build and are not advised.
+# This file is expected to be part of source control.
+com.fasterxml.jackson.core:jackson-annotations:2.14.2=classpath
+com.fasterxml.jackson.core:jackson-core:2.14.2=classpath
+com.fasterxml.jackson.core:jackson-databind:2.14.2=classpath
+com.fasterxml.jackson.module:jackson-module-parameter-names:2.14.2=classpath
+com.fasterxml.jackson:jackson-bom:2.14.2=classpath
+com.google.code.findbugs:jsr305:3.0.2=classpath
+io.spring.dependency-management:io.spring.dependency-management.gradle.plugin:1.1.6=classpath
+io.spring.gradle:dependency-management-plugin:1.1.6=classpath
+net.java.dev.jna:jna-platform:5.13.0=classpath
+net.java.dev.jna:jna:5.13.0=classpath
+org.antlr:antlr4-runtime:4.7.2=classpath
+org.apache.commons:commons-compress:1.25.0=classpath
+org.apache.httpcomponents.client5:httpclient5:5.3.1=classpath
+org.apache.httpcomponents.core5:httpcore5-h2:5.2.4=classpath
+org.apache.httpcomponents.core5:httpcore5:5.2.4=classpath
+org.slf4j:slf4j-api:1.7.36=classpath
+org.springframework.boot:org.springframework.boot.gradle.plugin:3.3.1=classpath
+org.springframework.boot:spring-boot-buildpack-platform:3.3.1=classpath
+org.springframework.boot:spring-boot-gradle-plugin:3.3.1=classpath
+org.springframework.boot:spring-boot-loader-tools:3.3.1=classpath
+org.springframework:spring-core:6.1.10=classpath
+org.springframework:spring-jcl:6.1.10=classpath
+org.tomlj:tomlj:1.0.0=classpath
+empty=
diff --git a/gradle.lockfile b/gradle.lockfile
new file mode 100644
index 0000000..7211c0f
--- /dev/null
+++ b/gradle.lockfile
@@ -0,0 +1,93 @@
+# This is a Gradle generated file for dependency locking.
+# Manual edits can break the build and are not advised.
+# This file is expected to be part of source control.
+ch.qos.logback:logback-classic:1.5.6=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+ch.qos.logback:logback-core:1.5.6=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+com.fasterxml.jackson.core:jackson-annotations:2.17.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+com.fasterxml.jackson:jackson-bom:2.17.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+com.fasterxml:classmate:1.7.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+com.github.docker-java:docker-java-api:3.3.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+com.github.docker-java:docker-java-transport-zerodep:3.3.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+com.github.docker-java:docker-java-transport:3.3.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+com.opencsv:opencsv:5.9=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+com.sun.istack:istack-commons-runtime:4.1.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+com.zaxxer:HikariCP:5.1.0=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+de.cronn:test-utils:1.1.0=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+io.micrometer:micrometer-commons:1.13.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+io.micrometer:micrometer-observation:1.13.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+io.smallrye:jandex:3.1.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+jakarta.annotation:jakarta.annotation-api:2.1.1=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+jakarta.inject:jakarta.inject-api:2.0.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+jakarta.persistence:jakarta.persistence-api:3.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+jakarta.transaction:jakarta.transaction-api:2.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+javax.xml.bind:jaxb-api:2.3.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+junit:junit:4.13.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+net.bytebuddy:byte-buddy:1.14.17=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+net.java.dev.jna:jna:5.13.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.antlr:antlr4-runtime:4.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.apache.commons:commons-collections4:4.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.apache.commons:commons-compress:1.24.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.apache.commons:commons-lang3:3.14.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.apache.commons:commons-text:1.11.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.apache.logging.log4j:log4j-api:2.23.1=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.apache.logging.log4j:log4j-to-slf4j:2.23.1=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testFixturesCompileClasspath
+org.aspectj:aspectjweaver:1.9.22=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.assertj:assertj-core:3.25.3=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.eclipse.angus:angus-activation:2.0.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.glassfish.jaxb:jaxb-core:4.0.5=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.glassfish.jaxb:jaxb-runtime:4.0.5=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.glassfish.jaxb:txw2:4.0.5=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.hamcrest:hamcrest-core:2.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.hamcrest:hamcrest:2.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.hibernate.orm:hibernate-core:6.5.2.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.jacoco:org.jacoco.agent:0.8.11=jacocoAgent,jacocoAnt
+org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt
+org.jacoco:org.jacoco.core:0.8.11=jacocoAnt
+org.jacoco:org.jacoco.report:0.8.11=jacocoAnt
+org.jboss.logging:jboss-logging:3.5.3.Final=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.jetbrains:annotations:17.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.junit.jupiter:junit-jupiter-api:5.10.2=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.junit.jupiter:junit-jupiter-engine:5.10.2=testRuntimeClasspath
+org.junit.jupiter:junit-jupiter-params:5.10.2=testCompileClasspath,testRuntimeClasspath
+org.junit.platform:junit-platform-commons:1.10.2=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.junit.platform:junit-platform-engine:1.10.2=testRuntimeClasspath
+org.junit.platform:junit-platform-launcher:1.10.2=testRuntimeClasspath
+org.junit:junit-bom:5.10.2=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.liquibase:liquibase-core:4.27.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.ow2.asm:asm-commons:9.6=jacocoAnt
+org.ow2.asm:asm-tree:9.6=jacocoAnt
+org.ow2.asm:asm:9.6=jacocoAnt
+org.rnorth.duct-tape:duct-tape:1.0.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.slf4j:jul-to-slf4j:2.0.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.slf4j:slf4j-api:2.0.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework.boot:spring-boot-autoconfigure:3.3.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework.boot:spring-boot-starter-aop:3.3.1=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework.boot:spring-boot-starter-data-jpa:3.3.1=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework.boot:spring-boot-starter-jdbc:3.3.1=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework.boot:spring-boot-starter-logging:3.3.1=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework.boot:spring-boot-starter:3.3.1=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework.boot:spring-boot:3.3.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework.data:spring-data-commons:3.3.1=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework.data:spring-data-jpa:3.3.1=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework:spring-aop:6.1.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework:spring-aspects:6.1.10=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework:spring-beans:6.1.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework:spring-context:6.1.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework:spring-core:6.1.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework:spring-expression:6.1.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework:spring-jcl:6.1.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework:spring-jdbc:6.1.10=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework:spring-orm:6.1.10=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.springframework:spring-tx:6.1.10=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.testcontainers:database-commons:1.19.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.testcontainers:jdbc:1.19.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.testcontainers:testcontainers:1.19.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+org.yaml:snakeyaml:2.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
+empty=annotationProcessor,developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor,testFixturesAnnotationProcessor
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..2c35211
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..dedd5d1
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..f5feea6
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,252 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
+' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..9b42019
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/postgresql/build.gradle b/postgresql/build.gradle
new file mode 100644
index 0000000..0bdc06f
--- /dev/null
+++ b/postgresql/build.gradle
@@ -0,0 +1,8 @@
+dependencies {
+ api rootProject
+
+ runtimeOnly 'org.testcontainers:postgresql'
+ runtimeOnly 'org.postgresql:postgresql'
+
+ testImplementation testFixtures(rootProject)
+}
diff --git a/postgresql/data/test/validation/HibernateSchemaExportTest/testExport.sql b/postgresql/data/test/validation/HibernateSchemaExportTest/testExport.sql
new file mode 100644
index 0000000..378f0f4
--- /dev/null
+++ b/postgresql/data/test/validation/HibernateSchemaExportTest/testExport.sql
@@ -0,0 +1,49 @@
+create type Color as enum ('BLUE','GREEN','RED');
+
+create cast (varchar as Color) with inout as implicit;
+
+create cast (Color as varchar) with inout as implicit;
+
+create type Count as enum ('ONE','THREE','TWO');
+
+create cast (varchar as Count) with inout as implicit;
+
+create cast (Count as varchar) with inout as implicit;
+
+create type Size as enum ('L','M','S','XL','XS');
+
+create cast (varchar as Size) with inout as implicit;
+
+create cast (Size as varchar) with inout as implicit;
+
+create table entity_with_enum1 (
+ id bigint not null,
+ count Count,
+ size Size,
+ primary key (id)
+);
+
+create table entity_with_enum2 (
+ id bigint not null,
+ color Color,
+ count Count,
+ primary key (id)
+);
+
+create table other_entity (
+ id bigint not null,
+ primary key (id)
+);
+
+create table test_entity (
+ id bigint not null,
+ other_id bigint not null,
+ description varchar(255),
+ name varchar(255) not null unique,
+ primary key (id)
+);
+
+alter table if exists test_entity
+ add constraint FKlfw5k4g68kprhjh5lujkkhi5a
+ foreign key (other_id)
+ references other_entity;
diff --git a/postgresql/data/test/validation/HibernateToLiquibaseDiffTest/testGenerateDiff_emptyLiquibaseChangelog.xml b/postgresql/data/test/validation/HibernateToLiquibaseDiffTest/testGenerateDiff_emptyLiquibaseChangelog.xml
new file mode 100644
index 0000000..cfd8e18
--- /dev/null
+++ b/postgresql/data/test/validation/HibernateToLiquibaseDiffTest/testGenerateDiff_emptyLiquibaseChangelog.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/postgresql/data/test/validation/HibernateToLiquibaseDiffTest/testGenerateDiff_fullLiquibaseChangelog.xml b/postgresql/data/test/validation/HibernateToLiquibaseDiffTest/testGenerateDiff_fullLiquibaseChangelog.xml
new file mode 100644
index 0000000..639b326
--- /dev/null
+++ b/postgresql/data/test/validation/HibernateToLiquibaseDiffTest/testGenerateDiff_fullLiquibaseChangelog.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/postgresql/gradle.lockfile b/postgresql/gradle.lockfile
new file mode 100644
index 0000000..cf3b6fa
--- /dev/null
+++ b/postgresql/gradle.lockfile
@@ -0,0 +1,96 @@
+# This is a Gradle generated file for dependency locking.
+# Manual edits can break the build and are not advised.
+# This file is expected to be part of source control.
+ch.qos.logback:logback-classic:1.5.6=testCompileClasspath,testRuntimeClasspath
+ch.qos.logback:logback-core:1.5.6=testCompileClasspath,testRuntimeClasspath
+com.fasterxml.jackson.core:jackson-annotations:2.17.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+com.fasterxml.jackson:jackson-bom:2.17.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+com.fasterxml:classmate:1.7.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath
+com.opencsv:opencsv:5.9=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+com.sun.istack:istack-commons-runtime:4.1.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+com.zaxxer:HikariCP:5.1.0=testCompileClasspath,testRuntimeClasspath
+de.cronn:test-utils:1.1.0=testCompileClasspath,testRuntimeClasspath
+de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testRuntimeClasspath
+io.micrometer:micrometer-commons:1.13.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+io.micrometer:micrometer-observation:1.13.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+io.smallrye:jandex:3.1.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+jakarta.annotation:jakarta.annotation-api:2.1.1=testCompileClasspath,testRuntimeClasspath
+jakarta.inject:jakarta.inject-api:2.0.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+jakarta.persistence:jakarta.persistence-api:3.1.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+jakarta.transaction:jakarta.transaction-api:2.0.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+javax.xml.bind:jaxb-api:2.3.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+net.bytebuddy:byte-buddy:1.14.17=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+net.java.dev.jna:jna:5.13.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.antlr:antlr4-runtime:4.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.apache.commons:commons-collections4:4.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.apache.commons:commons-compress:1.24.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.apache.commons:commons-lang3:3.14.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.apache.commons:commons-text:1.11.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.apache.logging.log4j:log4j-api:2.23.1=testCompileClasspath,testRuntimeClasspath
+org.apache.logging.log4j:log4j-to-slf4j:2.23.1=testCompileClasspath,testRuntimeClasspath
+org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
+org.aspectj:aspectjweaver:1.9.22=testCompileClasspath,testRuntimeClasspath
+org.assertj:assertj-core:3.25.3=testCompileClasspath,testRuntimeClasspath
+org.checkerframework:checker-qual:3.42.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.eclipse.angus:angus-activation:2.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.glassfish.jaxb:jaxb-core:4.0.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.glassfish.jaxb:jaxb-runtime:4.0.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.glassfish.jaxb:txw2:4.0.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.hibernate.orm:hibernate-core:6.5.2.Final=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.jacoco:org.jacoco.agent:0.8.11=jacocoAgent,jacocoAnt
+org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt
+org.jacoco:org.jacoco.core:0.8.11=jacocoAnt
+org.jacoco:org.jacoco.report:0.8.11=jacocoAnt
+org.jboss.logging:jboss-logging:3.5.3.Final=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.jetbrains:annotations:17.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.junit.jupiter:junit-jupiter-api:5.10.2=testCompileClasspath,testRuntimeClasspath
+org.junit.jupiter:junit-jupiter-engine:5.10.2=testRuntimeClasspath
+org.junit.jupiter:junit-jupiter-params:5.10.2=testCompileClasspath,testRuntimeClasspath
+org.junit.platform:junit-platform-commons:1.10.2=testCompileClasspath,testRuntimeClasspath
+org.junit.platform:junit-platform-engine:1.10.2=testRuntimeClasspath
+org.junit.platform:junit-platform-launcher:1.10.2=testRuntimeClasspath
+org.junit:junit-bom:5.10.2=testCompileClasspath,testRuntimeClasspath
+org.liquibase:liquibase-core:4.27.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
+org.ow2.asm:asm-commons:9.6=jacocoAnt
+org.ow2.asm:asm-tree:9.6=jacocoAnt
+org.ow2.asm:asm:9.6=jacocoAnt
+org.postgresql:postgresql:42.7.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.slf4j:jul-to-slf4j:2.0.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.slf4j:slf4j-api:2.0.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.springframework.boot:spring-boot-autoconfigure:3.3.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.springframework.boot:spring-boot-starter-aop:3.3.1=testCompileClasspath,testRuntimeClasspath
+org.springframework.boot:spring-boot-starter-data-jpa:3.3.1=testCompileClasspath,testRuntimeClasspath
+org.springframework.boot:spring-boot-starter-jdbc:3.3.1=testCompileClasspath,testRuntimeClasspath
+org.springframework.boot:spring-boot-starter-logging:3.3.1=testCompileClasspath,testRuntimeClasspath
+org.springframework.boot:spring-boot-starter:3.3.1=testCompileClasspath,testRuntimeClasspath
+org.springframework.boot:spring-boot:3.3.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.springframework.data:spring-data-commons:3.3.1=testCompileClasspath,testRuntimeClasspath
+org.springframework.data:spring-data-jpa:3.3.1=testCompileClasspath,testRuntimeClasspath
+org.springframework:spring-aop:6.1.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.springframework:spring-aspects:6.1.10=testCompileClasspath,testRuntimeClasspath
+org.springframework:spring-beans:6.1.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.springframework:spring-context:6.1.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.springframework:spring-core:6.1.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.springframework:spring-expression:6.1.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.springframework:spring-jcl:6.1.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.springframework:spring-jdbc:6.1.10=testCompileClasspath,testRuntimeClasspath
+org.springframework:spring-orm:6.1.10=testCompileClasspath,testRuntimeClasspath
+org.springframework:spring-tx:6.1.10=testCompileClasspath,testRuntimeClasspath
+org.testcontainers:database-commons:1.19.8=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.testcontainers:jdbc:1.19.8=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.testcontainers:postgresql:1.19.8=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
+org.yaml:snakeyaml:2.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+empty=annotationProcessor,developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor
diff --git a/postgresql/src/main/java/de/cronn/liquibase/changelog/generator/postgresql/HibernatePopulatedConfigForPostgres.java b/postgresql/src/main/java/de/cronn/liquibase/changelog/generator/postgresql/HibernatePopulatedConfigForPostgres.java
new file mode 100644
index 0000000..f586901
--- /dev/null
+++ b/postgresql/src/main/java/de/cronn/liquibase/changelog/generator/postgresql/HibernatePopulatedConfigForPostgres.java
@@ -0,0 +1,9 @@
+package de.cronn.liquibase.changelog.generator.postgresql;
+
+import org.springframework.context.annotation.PropertySource;
+
+import de.cronn.liquibase.changelog.generator.AbstractHibernatePopulatedConfig;
+
+@PropertySource("classpath:/de/cronn/liquibase/changelog/generator/postgresql/hibernate-populated-postgresql.properties")
+public abstract class HibernatePopulatedConfigForPostgres extends AbstractHibernatePopulatedConfig {
+}
diff --git a/postgresql/src/main/java/de/cronn/liquibase/changelog/generator/postgresql/HibernateToLiquibaseDiffForPostgres.java b/postgresql/src/main/java/de/cronn/liquibase/changelog/generator/postgresql/HibernateToLiquibaseDiffForPostgres.java
new file mode 100644
index 0000000..95adeaf
--- /dev/null
+++ b/postgresql/src/main/java/de/cronn/liquibase/changelog/generator/postgresql/HibernateToLiquibaseDiffForPostgres.java
@@ -0,0 +1,16 @@
+package de.cronn.liquibase.changelog.generator.postgresql;
+
+import de.cronn.liquibase.changelog.generator.HibernateToLiquibaseDiff;
+import liquibase.database.AbstractJdbcDatabase;
+import liquibase.database.core.PostgresDatabase;
+
+public class HibernateToLiquibaseDiffForPostgres extends HibernateToLiquibaseDiff {
+ public HibernateToLiquibaseDiffForPostgres(String changeSetAuthor) {
+ super(changeSetAuthor);
+ }
+
+ @Override
+ protected AbstractJdbcDatabase createDatabase() {
+ return new PostgresDatabase();
+ }
+}
diff --git a/postgresql/src/main/java/de/cronn/liquibase/changelog/generator/postgresql/LiquibasePopulatedConfigForPostgres.java b/postgresql/src/main/java/de/cronn/liquibase/changelog/generator/postgresql/LiquibasePopulatedConfigForPostgres.java
new file mode 100644
index 0000000..fe1c6c0
--- /dev/null
+++ b/postgresql/src/main/java/de/cronn/liquibase/changelog/generator/postgresql/LiquibasePopulatedConfigForPostgres.java
@@ -0,0 +1,9 @@
+package de.cronn.liquibase.changelog.generator.postgresql;
+
+import de.cronn.liquibase.changelog.generator.AbstractLiquibasePopulatedConfig;
+
+import org.springframework.context.annotation.PropertySource;
+
+@PropertySource("classpath:/de/cronn/liquibase/changelog/generator/postgresql/liquibase-populated-postgresql.properties")
+public class LiquibasePopulatedConfigForPostgres extends AbstractLiquibasePopulatedConfig {
+}
diff --git a/postgresql/src/main/resources/de/cronn/liquibase/changelog/generator/postgresql/hibernate-populated-postgresql.properties b/postgresql/src/main/resources/de/cronn/liquibase/changelog/generator/postgresql/hibernate-populated-postgresql.properties
new file mode 100644
index 0000000..7ae2b09
--- /dev/null
+++ b/postgresql/src/main/resources/de/cronn/liquibase/changelog/generator/postgresql/hibernate-populated-postgresql.properties
@@ -0,0 +1 @@
+spring.datasource.url=jdbc:tc:postgresql:16.1:///liquibase_test_hibernate_populated?TC_TMPFS=/var/lib/postgresql/data:rw
diff --git a/postgresql/src/main/resources/de/cronn/liquibase/changelog/generator/postgresql/liquibase-populated-postgresql.properties b/postgresql/src/main/resources/de/cronn/liquibase/changelog/generator/postgresql/liquibase-populated-postgresql.properties
new file mode 100644
index 0000000..cb52028
--- /dev/null
+++ b/postgresql/src/main/resources/de/cronn/liquibase/changelog/generator/postgresql/liquibase-populated-postgresql.properties
@@ -0,0 +1 @@
+spring.datasource.url=jdbc:tc:postgresql:16.1:///liquibase_test_liquibase_populated?TC_TMPFS=/var/lib/postgresql/data:rw
diff --git a/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/Color.java b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/Color.java
new file mode 100644
index 0000000..628e06f
--- /dev/null
+++ b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/Color.java
@@ -0,0 +1,5 @@
+package de.cronn.liquibase.changelog.generator.model;
+
+public enum Color {
+ RED, GREEN, BLUE
+}
diff --git a/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/Count.java b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/Count.java
new file mode 100644
index 0000000..76cb36e
--- /dev/null
+++ b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/Count.java
@@ -0,0 +1,5 @@
+package de.cronn.liquibase.changelog.generator.model;
+
+public enum Count {
+ ONE, TWO, THREE
+}
diff --git a/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/EntityWithEnum1.java b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/EntityWithEnum1.java
new file mode 100644
index 0000000..b5f7cc4
--- /dev/null
+++ b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/EntityWithEnum1.java
@@ -0,0 +1,19 @@
+package de.cronn.liquibase.changelog.generator.model;
+
+import org.hibernate.annotations.JdbcType;
+import org.hibernate.dialect.PostgreSQLEnumJdbcType;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+
+@Entity
+public class EntityWithEnum1 {
+ @Id
+ private Long id;
+
+ @JdbcType(PostgreSQLEnumJdbcType.class)
+ private Count count;
+
+ @JdbcType(PostgreSQLEnumJdbcType.class)
+ private Size size;
+}
diff --git a/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/EntityWithEnum2.java b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/EntityWithEnum2.java
new file mode 100644
index 0000000..dee7514
--- /dev/null
+++ b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/EntityWithEnum2.java
@@ -0,0 +1,19 @@
+package de.cronn.liquibase.changelog.generator.model;
+
+import org.hibernate.annotations.JdbcType;
+import org.hibernate.dialect.PostgreSQLEnumJdbcType;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+
+@Entity
+public class EntityWithEnum2 {
+ @Id
+ private Long id;
+
+ @JdbcType(PostgreSQLEnumJdbcType.class)
+ private Count count;
+
+ @JdbcType(PostgreSQLEnumJdbcType.class)
+ private Color color;
+}
diff --git a/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/Size.java b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/Size.java
new file mode 100644
index 0000000..510e78f
--- /dev/null
+++ b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/model/Size.java
@@ -0,0 +1,5 @@
+package de.cronn.liquibase.changelog.generator.model;
+
+public enum Size {
+ XS, S, M, L, XL
+}
diff --git a/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/postgresql/HibernatePopulatedConfigForPostgresWithTestModel.java b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/postgresql/HibernatePopulatedConfigForPostgresWithTestModel.java
new file mode 100644
index 0000000..204c11d
--- /dev/null
+++ b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/postgresql/HibernatePopulatedConfigForPostgresWithTestModel.java
@@ -0,0 +1,7 @@
+package de.cronn.liquibase.changelog.generator.postgresql;
+
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+
+@EntityScan("de.cronn.liquibase.changelog.generator.model")
+public class HibernatePopulatedConfigForPostgresWithTestModel extends HibernatePopulatedConfigForPostgres {
+}
diff --git a/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/postgresql/HibernateSchemaExportTest.java b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/postgresql/HibernateSchemaExportTest.java
new file mode 100644
index 0000000..58e4434
--- /dev/null
+++ b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/postgresql/HibernateSchemaExportTest.java
@@ -0,0 +1,17 @@
+package de.cronn.liquibase.changelog.generator.postgresql;
+
+import de.cronn.liquibase.changelog.generator.BaseTest;
+
+import org.junit.jupiter.api.Test;
+
+import de.cronn.assertions.validationfile.FileExtensions;
+import de.cronn.liquibase.changelog.generator.HibernateSchemaExport;
+
+class HibernateSchemaExportTest extends BaseTest {
+ @Test
+ void testExport() {
+ String hibernateSchema = new HibernateSchemaExport(HibernatePopulatedConfigForPostgresWithTestModel.class)
+ .export();
+ assertWithFile(hibernateSchema, FileExtensions.SQL);
+ }
+}
diff --git a/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/postgresql/HibernateToLiquibaseDiffTest.java b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/postgresql/HibernateToLiquibaseDiffTest.java
new file mode 100644
index 0000000..0c0051e
--- /dev/null
+++ b/postgresql/src/test/java/de/cronn/liquibase/changelog/generator/postgresql/HibernateToLiquibaseDiffTest.java
@@ -0,0 +1,40 @@
+package de.cronn.liquibase.changelog.generator.postgresql;
+
+import de.cronn.liquibase.changelog.generator.BaseTest;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.PropertySource;
+
+import de.cronn.assertions.validationfile.FileExtensions;
+import de.cronn.assertions.validationfile.normalization.SimpleRegexReplacement;
+import de.cronn.assertions.validationfile.normalization.ValidationNormalizer;
+
+class HibernateToLiquibaseDiffTest extends BaseTest {
+
+ @Test
+ void testGenerateDiff_emptyLiquibaseChangelog() {
+ String diff = new HibernateToLiquibaseDiffForPostgres("Jane Doe")
+ .generateDiff(HibernatePopulatedConfigForPostgresWithTestModel.class, EmptyLiquibaseConfig.class);
+ assertWithFile(diff, maskGeneratedId(), FileExtensions.XML);
+ }
+
+ @Test
+ void testGenerateDiff_fullLiquibaseChangelog() {
+ String diff = new HibernateToLiquibaseDiffForPostgres("Jane Doe")
+ .generateDiff(HibernatePopulatedConfigForPostgresWithTestModel.class, FullLiquibaseConfig.class);
+ assertWithFile(diff, maskGeneratedId(), FileExtensions.XML);
+ }
+
+ @PropertySource("classpath:/empty-changelog.properties")
+ private static class EmptyLiquibaseConfig extends LiquibasePopulatedConfigForPostgres {
+ }
+
+ @PropertySource("classpath:/full-changelog.properties")
+ private static class FullLiquibaseConfig extends LiquibasePopulatedConfigForPostgres {
+ }
+
+ private static ValidationNormalizer maskGeneratedId() {
+ return new SimpleRegexReplacement("id=\"\\d{10,}-", "id=\"[MASKED]-");
+ }
+}
+
diff --git a/postgresql/src/test/resources/empty-changelog.properties b/postgresql/src/test/resources/empty-changelog.properties
new file mode 100644
index 0000000..b0e9943
--- /dev/null
+++ b/postgresql/src/test/resources/empty-changelog.properties
@@ -0,0 +1 @@
+spring.liquibase.change-log=classpath:/migrations/empty-changelog.xml
diff --git a/postgresql/src/test/resources/full-changelog.properties b/postgresql/src/test/resources/full-changelog.properties
new file mode 100644
index 0000000..81b5768
--- /dev/null
+++ b/postgresql/src/test/resources/full-changelog.properties
@@ -0,0 +1 @@
+spring.liquibase.change-log=classpath:/migrations/full-changelog.xml
diff --git a/postgresql/src/test/resources/migrations/empty-changelog.xml b/postgresql/src/test/resources/migrations/empty-changelog.xml
new file mode 100644
index 0000000..2462091
--- /dev/null
+++ b/postgresql/src/test/resources/migrations/empty-changelog.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/postgresql/src/test/resources/migrations/full-changelog.xml b/postgresql/src/test/resources/migrations/full-changelog.xml
new file mode 100644
index 0000000..ab4edbc
--- /dev/null
+++ b/postgresql/src/test/resources/migrations/full-changelog.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ create type Count as enum ('ONE','THREE','TWO')
+ create type Size as enum ('L','M','S','XL','XS')
+ create type Color as enum ('BLUE','GREEN','RED')
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/release.sh b/release.sh
new file mode 100755
index 0000000..f43cd3d
--- /dev/null
+++ b/release.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+./gradlew --no-daemon clean build publish
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..30e903b
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,5 @@
+rootProject.name = "liquibase-changelog-generator"
+
+include "postgresql"
+
+rootProject.children.each { it.name = rootProject.name + "-" + it.name }
diff --git a/src/main/java/de/cronn/liquibase/changelog/generator/AbstractHibernatePopulatedConfig.java b/src/main/java/de/cronn/liquibase/changelog/generator/AbstractHibernatePopulatedConfig.java
new file mode 100644
index 0000000..9cd4716
--- /dev/null
+++ b/src/main/java/de/cronn/liquibase/changelog/generator/AbstractHibernatePopulatedConfig.java
@@ -0,0 +1,11 @@
+package de.cronn.liquibase.changelog.generator;
+
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
+import org.springframework.context.annotation.PropertySource;
+
+@ImportAutoConfiguration({ DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
+@PropertySource("classpath:/de/cronn/liquibase/changelog/generator/hibernate-populated.properties")
+public abstract class AbstractHibernatePopulatedConfig {
+}
diff --git a/src/main/java/de/cronn/liquibase/changelog/generator/AbstractLiquibasePopulatedConfig.java b/src/main/java/de/cronn/liquibase/changelog/generator/AbstractLiquibasePopulatedConfig.java
new file mode 100644
index 0000000..f3c5a86
--- /dev/null
+++ b/src/main/java/de/cronn/liquibase/changelog/generator/AbstractLiquibasePopulatedConfig.java
@@ -0,0 +1,9 @@
+package de.cronn.liquibase.changelog.generator;
+
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration;
+
+@ImportAutoConfiguration({ DataSourceAutoConfiguration.class, LiquibaseAutoConfiguration.class })
+public abstract class AbstractLiquibasePopulatedConfig {
+}
diff --git a/src/main/java/de/cronn/liquibase/changelog/generator/HibernateIntegratorForSchemaExport.java b/src/main/java/de/cronn/liquibase/changelog/generator/HibernateIntegratorForSchemaExport.java
new file mode 100644
index 0000000..ecb3148
--- /dev/null
+++ b/src/main/java/de/cronn/liquibase/changelog/generator/HibernateIntegratorForSchemaExport.java
@@ -0,0 +1,25 @@
+package de.cronn.liquibase.changelog.generator;
+
+import org.hibernate.boot.Metadata;
+import org.hibernate.boot.spi.BootstrapContext;
+import org.hibernate.engine.spi.SessionFactoryImplementor;
+import org.hibernate.service.spi.SessionFactoryServiceRegistry;
+
+public class HibernateIntegratorForSchemaExport implements org.hibernate.integrator.spi.Integrator {
+
+ static Metadata metadata;
+
+ @Override
+ public void integrate(Metadata metadata, BootstrapContext bootstrapContext, SessionFactoryImplementor sessionFactory) {
+ HibernateIntegratorForSchemaExport.metadata = metadata;
+ }
+
+ @Override
+ public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
+ metadata = null;
+ }
+
+ public static Metadata getMetadata() {
+ return metadata;
+ }
+}
diff --git a/src/main/java/de/cronn/liquibase/changelog/generator/HibernateSchemaExport.java b/src/main/java/de/cronn/liquibase/changelog/generator/HibernateSchemaExport.java
new file mode 100644
index 0000000..62d62c1
--- /dev/null
+++ b/src/main/java/de/cronn/liquibase/changelog/generator/HibernateSchemaExport.java
@@ -0,0 +1,165 @@
+package de.cronn.liquibase.changelog.generator;
+
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.commons.lang3.StringUtils;
+import org.hibernate.boot.Metadata;
+import org.hibernate.boot.registry.StandardServiceRegistry;
+import org.hibernate.boot.spi.MetadataImplementor;
+import org.hibernate.cfg.AvailableSettings;
+import org.hibernate.engine.config.spi.ConfigurationService;
+import org.hibernate.tool.schema.SourceType;
+import org.hibernate.tool.schema.TargetType;
+import org.hibernate.tool.schema.internal.ExceptionHandlerHaltImpl;
+import org.hibernate.tool.schema.internal.exec.ScriptTargetOutputToWriter;
+import org.hibernate.tool.schema.spi.ContributableMatcher;
+import org.hibernate.tool.schema.spi.ExceptionHandler;
+import org.hibernate.tool.schema.spi.ExecutionOptions;
+import org.hibernate.tool.schema.spi.SchemaCreator;
+import org.hibernate.tool.schema.spi.SchemaManagementTool;
+import org.hibernate.tool.schema.spi.ScriptSourceInput;
+import org.hibernate.tool.schema.spi.ScriptTargetOutput;
+import org.hibernate.tool.schema.spi.SourceDescriptor;
+import org.hibernate.tool.schema.spi.TargetDescriptor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+// This implementation is based on org.hibernate.tool.hbm2ddl.SchemaExport from the "hibernate-ant" dependency.
+public class HibernateSchemaExport {
+
+ private static final Logger log = LoggerFactory.getLogger(HibernateSchemaExport.class);
+
+ private final Class extends AbstractHibernatePopulatedConfig> hibernatePopulatedConfigClass;
+
+ public HibernateSchemaExport(Class extends AbstractHibernatePopulatedConfig> hibernatePopulatedConfigClass) {
+ this.hibernatePopulatedConfigClass = hibernatePopulatedConfigClass;
+ }
+
+ public String export() {
+ try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(hibernatePopulatedConfigClass)) {
+ log.trace("Created application context {}", context);
+ String schemaExport = exportSchema();
+ return normalizeSchemaDumpFile(schemaExport);
+ }
+ }
+
+ protected String exportSchema() {
+ Metadata metadata = HibernateIntegratorForSchemaExport.getMetadata();
+
+ Map config = buildConfig();
+
+ StringWriter writer = new StringWriter();
+
+ SchemaCreator schemaCreator = getSchemaCreator(config);
+ schemaCreator.doCreation(metadata, new ExecutionOptionsForSchemaExport(config), ContributableMatcher.ALL,
+ new MetadataSourceDescriptor(), new ScriptTargetDescriptor(writer));
+
+ return writer.toString();
+ }
+
+ protected String normalizeSchemaDumpFile(String schemaExport) {
+ String schemaExportWithNormalizedWhitespaces = normalizeIndentations(schemaExport);
+ return sortCreateTypeStatements(schemaExportWithNormalizedWhitespaces);
+ }
+
+ protected String normalizeIndentations(String schemaExport) {
+ return Arrays.stream(schemaExport.split("\r?\n"))
+ .map(line -> {
+ line = line.replaceFirst("^ {4}", "");
+ line = line.replaceFirst("^ {3}(\\w)", " $1");
+ return StringUtils.stripEnd(line, null);
+ })
+ .collect(Collectors.joining("\n"));
+ }
+
+ protected String sortCreateTypeStatements(String schemaExport) {
+ String partToSort = StringUtils.substringBefore(schemaExport, "create table");
+ String partAfterSort = schemaExport.substring(partToSort.length());
+ String sortedPart =
+ Stream.of(partToSort.split("create type"))
+ .sorted()
+ .collect(Collectors.joining("create type"));
+ return sortedPart + partAfterSort;
+ }
+
+ protected Map buildConfig() {
+ ConfigurationService configurationService = getServiceRegistry().requireService(ConfigurationService.class);
+ Map config = new LinkedHashMap<>(configurationService.getSettings());
+ config.put(AvailableSettings.FORMAT_SQL, true);
+ return config;
+ }
+
+ protected SchemaCreator getSchemaCreator(Map config) {
+ return getServiceRegistry().requireService(SchemaManagementTool.class).getSchemaCreator(config);
+ }
+
+ protected StandardServiceRegistry getServiceRegistry() {
+ MetadataImplementor metadata = (MetadataImplementor) HibernateIntegratorForSchemaExport.getMetadata();
+ return metadata.getMetadataBuildingOptions().getServiceRegistry();
+ }
+
+ @SuppressWarnings("ClassCanBeRecord")
+ private static class ScriptTargetDescriptor implements TargetDescriptor {
+
+ private final Writer writer;
+
+ public ScriptTargetDescriptor(Writer writer) {
+ this.writer = writer;
+ }
+
+ @Override
+ public EnumSet getTargetTypes() {
+ return EnumSet.of(TargetType.SCRIPT);
+ }
+
+ @Override
+ public ScriptTargetOutput getScriptTargetOutput() {
+ return new ScriptTargetOutputToWriter(writer);
+ }
+ }
+
+ private static class MetadataSourceDescriptor implements SourceDescriptor {
+ @Override
+ public SourceType getSourceType() {
+ return SourceType.METADATA;
+ }
+
+ @Override
+ public ScriptSourceInput getScriptSourceInput() {
+ return null;
+ }
+ }
+
+ @SuppressWarnings("ClassCanBeRecord")
+ private static class ExecutionOptionsForSchemaExport implements ExecutionOptions {
+ private final Map config;
+
+ public ExecutionOptionsForSchemaExport(Map config) {
+ this.config = config;
+ }
+
+ @Override
+ public Map getConfigurationValues() {
+ return config;
+ }
+
+ @Override
+ public boolean shouldManageNamespaces() {
+ return false;
+ }
+
+ @Override
+ public ExceptionHandler getExceptionHandler() {
+ return ExceptionHandlerHaltImpl.INSTANCE;
+ }
+ }
+}
diff --git a/src/main/java/de/cronn/liquibase/changelog/generator/HibernateToLiquibaseDiff.java b/src/main/java/de/cronn/liquibase/changelog/generator/HibernateToLiquibaseDiff.java
new file mode 100644
index 0000000..ca75e8a
--- /dev/null
+++ b/src/main/java/de/cronn/liquibase/changelog/generator/HibernateToLiquibaseDiff.java
@@ -0,0 +1,156 @@
+package de.cronn.liquibase.changelog.generator;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.sql.Connection;
+import java.util.stream.Collectors;
+
+import javax.sql.DataSource;
+
+import org.slf4j.bridge.SLF4JBridgeHandler;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.support.GenericApplicationContext;
+
+import liquibase.database.AbstractJdbcDatabase;
+import liquibase.database.Database;
+import liquibase.database.jvm.JdbcConnection;
+import liquibase.diff.DiffGeneratorFactory;
+import liquibase.diff.DiffResult;
+import liquibase.diff.ObjectDifferences;
+import liquibase.diff.compare.CompareControl;
+import liquibase.diff.output.DiffOutputControl;
+import liquibase.diff.output.changelog.DiffToChangeLog;
+import liquibase.snapshot.DatabaseSnapshot;
+import liquibase.structure.DatabaseObject;
+import liquibase.structure.core.Column;
+import liquibase.structure.core.ForeignKey;
+import liquibase.structure.core.Index;
+import liquibase.structure.core.PrimaryKey;
+import liquibase.structure.core.Table;
+
+public abstract class HibernateToLiquibaseDiff {
+
+ private final String changeSetAuthor;
+
+ protected HibernateToLiquibaseDiff(String changeSetAuthor) {
+ SLF4JBridgeHandler.removeHandlersForRootLogger();
+ SLF4JBridgeHandler.install();
+ this.changeSetAuthor = changeSetAuthor;
+ }
+
+ public String generateDiff(Class extends AbstractHibernatePopulatedConfig> hibernatePopulatedConfigClass,
+ Class extends AbstractLiquibasePopulatedConfig> liquibasePopulatedConfigClass) {
+ try (AnnotationConfigApplicationContext hibernatePopulatedContext = new AnnotationConfigApplicationContext(hibernatePopulatedConfigClass);
+ Connection hibernatePopulatedConnection = getConnection(hibernatePopulatedContext);
+ AnnotationConfigApplicationContext liquibasePopulatedContext = new AnnotationConfigApplicationContext(liquibasePopulatedConfigClass);
+ Connection liquibasePopulatedConnection = getConnection(liquibasePopulatedContext)) {
+ DiffResult diffResult = generateDiff(hibernatePopulatedConnection, liquibasePopulatedConnection);
+ return writeDiffResultToChangeLog(diffResult);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected Connection getConnection(GenericApplicationContext context) throws Exception {
+ return context.getBean(DataSource.class).getConnection();
+ }
+
+ protected DiffResult generateDiff(Connection reference, Connection target) throws Exception {
+ Database referenceDatabase = newDatabase(reference);
+ Database targetDatabase = newDatabase(target);
+
+ DiffResult diff = DiffGeneratorFactory.getInstance().compare(referenceDatabase, targetDatabase, CompareControl.STANDARD);
+ return filterDiffResult(diff);
+ }
+
+ protected DiffResult filterDiffResult(DiffResult diffResult) {
+ DatabaseSnapshot referenceDatabaseSnapshot = diffResult.getReferenceSnapshot();
+ DatabaseSnapshot comparisonDatabaseSnapshot = diffResult.getComparisonSnapshot();
+ CompareControl compareControl = diffResult.getCompareControl();
+
+ DiffResult result = new DiffResult(referenceDatabaseSnapshot, comparisonDatabaseSnapshot, compareControl);
+
+ diffResult.getChangedObjects().forEach((obj, differences) -> {
+ handleChangedObject(result, obj, differences);
+ });
+
+ for (DatabaseObject obj : diffResult.getMissingObjects()) {
+ handleMissingObject(result, obj);
+ }
+
+ for (DatabaseObject obj : diffResult.getUnexpectedObjects()) {
+ handleUnexpectedObject(result, obj);
+ }
+
+ return result;
+ }
+
+ protected void handleChangedObject(DiffResult result, DatabaseObject obj, ObjectDifferences differences) {
+ result.addChangedObject(obj, differences);
+ }
+
+ protected void handleMissingObject(DiffResult result, DatabaseObject obj) {
+ generateNewKeyAndIndexNames(obj);
+ result.addMissingObject(obj);
+ }
+
+ protected void generateNewKeyAndIndexNames(DatabaseObject obj) {
+ if (obj instanceof PrimaryKey primaryKey) {
+ primaryKey.setName(generateNewPrimaryKeyName(primaryKey));
+ }
+ if (obj instanceof ForeignKey foreignKey) {
+ foreignKey.setName(generateNewForeignKeyName(foreignKey));
+ }
+ if (obj instanceof Index index) {
+ index.setName(generateNewIndexName(index));
+ }
+ }
+
+ protected void handleUnexpectedObject(DiffResult result, DatabaseObject obj) {
+ result.addUnexpectedObject(obj);
+ }
+
+ protected String writeDiffResultToChangeLog(DiffResult result) throws Exception {
+ DiffOutputControl diffOutputControl = newDiffOutputControl();
+
+ DiffToChangeLog diffToChangeLog = new DiffToChangeLog(result, diffOutputControl);
+ diffToChangeLog.setChangeSetAuthor(changeSetAuthor);
+
+ String encoding = StandardCharsets.UTF_8.name();
+ try (ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+ PrintStream out = new PrintStream(bytesOut, true, encoding)) {
+ diffToChangeLog.print(out);
+ out.flush();
+ return bytesOut.toString(encoding);
+ }
+ }
+
+ protected DiffOutputControl newDiffOutputControl() {
+ return new DiffOutputControl(false, false, false, null);
+ }
+
+ protected String generateNewPrimaryKeyName(PrimaryKey primaryKey) {
+ return "pk_" + primaryKey.getTable().getName();
+ }
+
+ protected String generateNewForeignKeyName(ForeignKey foreignKey) {
+ Table primaryKeyTable = foreignKey.getPrimaryKeyTable();
+ Table foreignKeyTable = foreignKey.getForeignKeyTable();
+ return "fk_" + foreignKeyTable.getName() + "_" + primaryKeyTable.getName();
+ }
+
+ protected String generateNewIndexName(Index index) {
+ return index.getColumns().stream()
+ .map(Column::getName)
+ .collect(Collectors.joining("_", "idx_" + index.getRelation().getName() + "_", ""));
+ }
+
+ protected Database newDatabase(Connection connection) {
+ AbstractJdbcDatabase database = createDatabase();
+ database.setConnection(new JdbcConnection(connection));
+ return database;
+ }
+
+ protected abstract AbstractJdbcDatabase createDatabase();
+}
diff --git a/src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator b/src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator
new file mode 100644
index 0000000..77697f8
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator
@@ -0,0 +1 @@
+de.cronn.liquibase.changelog.generator.HibernateIntegratorForSchemaExport
diff --git a/src/main/resources/de/cronn/liquibase/changelog/generator/hibernate-populated.properties b/src/main/resources/de/cronn/liquibase/changelog/generator/hibernate-populated.properties
new file mode 100644
index 0000000..530896f
--- /dev/null
+++ b/src/main/resources/de/cronn/liquibase/changelog/generator/hibernate-populated.properties
@@ -0,0 +1 @@
+spring.jpa.hibernate.ddl-auto=create
diff --git a/src/testFixtures/java/de/cronn/liquibase/changelog/generator/BaseTest.java b/src/testFixtures/java/de/cronn/liquibase/changelog/generator/BaseTest.java
new file mode 100644
index 0000000..07885d3
--- /dev/null
+++ b/src/testFixtures/java/de/cronn/liquibase/changelog/generator/BaseTest.java
@@ -0,0 +1,33 @@
+package de.cronn.liquibase.changelog.generator;
+
+import org.assertj.core.api.SoftAssertions;
+import org.assertj.core.api.junit.jupiter.InjectSoftAssertions;
+import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import de.cronn.assertions.validationfile.junit5.JUnit5ValidationFileAssertions;
+
+@ExtendWith(SoftAssertionsExtension.class)
+public abstract class BaseTest implements JUnit5ValidationFileAssertions {
+ @InjectSoftAssertions
+ private SoftAssertions softly;
+
+ private ValidationFilenameHelper validationFilenameHelper;
+
+ @Override
+ public FailedAssertionHandler failedAssertionHandler() {
+ return callable -> softly.check(callable::call);
+ }
+
+ @BeforeEach
+ void storeTestInfo(TestInfo testInfo) {
+ this.validationFilenameHelper = new ValidationFilenameHelper(testInfo);
+ }
+
+ @Override
+ public String getTestName() {
+ return validationFilenameHelper.getTestName();
+ }
+}
diff --git a/src/testFixtures/java/de/cronn/liquibase/changelog/generator/ValidationFilenameHelper.java b/src/testFixtures/java/de/cronn/liquibase/changelog/generator/ValidationFilenameHelper.java
new file mode 100644
index 0000000..0284dfe
--- /dev/null
+++ b/src/testFixtures/java/de/cronn/liquibase/changelog/generator/ValidationFilenameHelper.java
@@ -0,0 +1,42 @@
+package de.cronn.liquibase.changelog.generator;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.TestInfo;
+
+final class ValidationFilenameHelper {
+
+ private final TestInfo testInfo;
+
+ ValidationFilenameHelper(TestInfo testInfo) {
+ this.testInfo = testInfo;
+ }
+
+ String getTestName() {
+ List classes = ValidationFilenameHelper.classHierarchy(getTestClass());
+ return String.join("/", classes) + "/" + getTestMethod().getName();
+ }
+
+ private static List classHierarchy(Class> aClass) {
+ List classHierarchy = new ArrayList<>();
+ classHierarchy.add(aClass.getSimpleName());
+ Class> enclosingClass = aClass.getEnclosingClass();
+ while (enclosingClass != null) {
+ classHierarchy.add(enclosingClass.getSimpleName());
+ enclosingClass = enclosingClass.getEnclosingClass();
+ }
+ Collections.reverse(classHierarchy);
+ return classHierarchy;
+ }
+
+ private Method getTestMethod() {
+ return testInfo.getTestMethod().orElseThrow();
+ }
+
+ private Class> getTestClass() {
+ return testInfo.getTestClass().orElseThrow();
+ }
+}
diff --git a/src/testFixtures/java/de/cronn/liquibase/changelog/generator/model/OtherEntity.java b/src/testFixtures/java/de/cronn/liquibase/changelog/generator/model/OtherEntity.java
new file mode 100644
index 0000000..03f48c7
--- /dev/null
+++ b/src/testFixtures/java/de/cronn/liquibase/changelog/generator/model/OtherEntity.java
@@ -0,0 +1,16 @@
+package de.cronn.liquibase.changelog.generator.model;
+
+import java.util.List;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.OneToMany;
+
+@Entity
+public class OtherEntity {
+ @Id
+ private Long id;
+
+ @OneToMany(mappedBy = "other")
+ private List owningEntities;
+}
diff --git a/src/testFixtures/java/de/cronn/liquibase/changelog/generator/model/TestEntity.java b/src/testFixtures/java/de/cronn/liquibase/changelog/generator/model/TestEntity.java
new file mode 100644
index 0000000..55043c7
--- /dev/null
+++ b/src/testFixtures/java/de/cronn/liquibase/changelog/generator/model/TestEntity.java
@@ -0,0 +1,20 @@
+package de.cronn.liquibase.changelog.generator.model;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+
+@Entity
+public class TestEntity {
+ @Id
+ private Long id;
+
+ @Column(nullable = false, unique = true)
+ private String name;
+
+ private String description;
+
+ @ManyToOne(optional = false)
+ private OtherEntity other;
+}
diff --git a/updateDependencies.sh b/updateDependencies.sh
new file mode 100755
index 0000000..45c2a7a
--- /dev/null
+++ b/updateDependencies.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+# We remove the lockfiles as a workaround to inform the user of a failed dependency lock update.
+# This is required as Gradle currently exits successfully even in case of errors.
+rm **/*.lockfile
+./gradlew dependencies liquibase-changelog-generator-postgresql:dependencies --refresh-dependencies --update-locks '*:*'