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 hibernatePopulatedConfigClass; + + public HibernateSchemaExport(Class 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 hibernatePopulatedConfigClass, + Class 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 '*:*'