diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..fbc73fcb5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,68 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + workflow-actions: + patterns: + - "*" + allow: + - dependency-name: "actions/*" + - dependency-name: "redhat-actions/*" + + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "weekly" + day: "tuesday" + open-pull-requests-limit: 20 + groups: + hibernate-validator: + patterns: + - "org.hibernate.validator*" + - "org.glassfish.expressly*" + hibernate: + patterns: + - "org.hibernate*" + vertx: + patterns: + - "io.vertx*" + mutiny: + patterns: + - "io.smallrye.reactive*" + # Testcontainers plus the JDBC driver we need for testing + testcontainers: + patterns: + - "org.testcontainers*" + - "com.ibm.db2*" + - "com.microsoft.sqlserver*" + - "org.postgresql*" + - "con.ongres.scram*" + - "com.fasterxml.jackson.core*" + - "com.mysql*" + - "org.mariadb.jdbc*" + + ignore: + # For Hibernate Validator, we will need to update major version manually as needed (but we only use it in tests) + - dependency-name: "org.glassfish.expressly*" + update-types: ["version-update-:semver-major"] + # Only patches for Hibernate ORM and Vert.x + - dependency-name: "org.hibernate*" + update-types: ["version-update:semver-major", "version-update:semver-minor"] + - dependency-name: "io.vertx*" + update-types: ["version-update:semver-major", "version-update:semver-minor"] + + # Dockerfiles in tooling/docker/, and database services we use for examples (MySQL and PostgreSQL) + - package-ecosystem: "docker" + directory: "/tooling/docker" + schedule: + interval: "weekly" + allow: + - dependency-type: "all" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c90f0b9c..9a9333466 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,12 +5,17 @@ on: branches: - 'main' - 'wip/**' + - '2.*' + - '3.*' tags: - '2.*' + - '3.*' pull_request: branches: - 'main' - 'wip/**' + - '2.*' + - '3.*' # For building snapshots workflow_call: inputs: @@ -44,8 +49,7 @@ jobs: services: # Label used to access the service container mysql: - # Docker Hub image - image: mysql:8.4.0 + image: container-registry.oracle.com/mysql/community-server:9.3.0 env: MYSQL_ROOT_PASSWORD: hreact MYSQL_DATABASE: hreact @@ -61,7 +65,7 @@ jobs: - 3306:3306 postgres: # Docker Hub image - image: postgres:16.3 + image: postgres:17.4 env: POSTGRES_DB: hreact POSTGRES_USER: hreact @@ -76,7 +80,7 @@ jobs: - 5432:5432 steps: - name: Checkout ${{ inputs.branch }} - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ inputs.branch }} - name: Get year/month for cache key @@ -85,7 +89,7 @@ jobs: echo "::set-output name=yearmonth::$(/bin/date -u "+%Y-%m")" shell: bash - name: Cache Gradle downloads - uses: actions/cache@v2 + uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 id: cache-gradle with: path: | @@ -95,7 +99,7 @@ jobs: # refresh cache every month to avoid unlimited growth key: gradle-examples-${{ matrix.db }}-${{ steps.get-date.outputs.yearmonth }} - name: Set up JDK 11 - uses: actions/setup-java@v2.2.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: distribution: 'temurin' java-version: 11 @@ -104,7 +108,7 @@ jobs: - name: Run examples in '${{ matrix.example }}' on ${{ matrix.db }} run: ./gradlew :${{ matrix.example }}:runAllExamplesOn${{ matrix.db }} - name: Upload reports (if build failed) - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 if: failure() with: name: reports-examples-${{ matrix.db }} @@ -118,7 +122,7 @@ jobs: db: [ 'MariaDB', 'MySQL', 'PostgreSQL', 'MSSQLServer', 'CockroachDB', 'Db2', 'Oracle' ] steps: - name: Checkout ${{ inputs.branch }} - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ inputs.branch }} - name: Get year/month for cache key @@ -127,7 +131,7 @@ jobs: echo "::set-output name=yearmonth::$(/bin/date -u "+%Y-%m")" shell: bash - name: Cache Gradle downloads - uses: actions/cache@v2 + uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 id: cache-gradle with: path: | @@ -137,7 +141,7 @@ jobs: # refresh cache every month to avoid unlimited growth key: gradle-db-${{ matrix.db }}-${{ steps.get-date.outputs.yearmonth }} - name: Set up JDK 11 - uses: actions/setup-java@v2.2.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: distribution: 'temurin' java-version: 11 @@ -146,7 +150,7 @@ jobs: - name: Build and Test with ${{ matrix.db }} run: ./gradlew build -PshowStandardOutput -Pdocker -Pdb=${{ matrix.db }} - name: Upload reports (if build failed) - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 if: failure() with: name: reports-db-${{ matrix.db }} @@ -172,12 +176,12 @@ jobs: # and it's useful to test that. - { name: "20", java_version_numeric: 20, jvm_args: '--enable-preview' } - { name: "21", java_version_numeric: 21, jvm_args: '--enable-preview' } - - { name: "23", java_version_numeric: 23, from: 'jdk.java.net', jvm_args: '--enable-preview' } - - { name: "24-ea", java_version_numeric: 24, from: 'jdk.java.net', jvm_args: '--enable-preview' } - - { name: "25-ea", java_version_numeric: 25, from: 'jdk.java.net', jvm_args: '--enable-preview' } + - { name: "24", java_version_numeric: 24, jvm_args: '--enable-preview' } + - { name: "25", java_version_numeric: 25, from: 'jdk.java.net', jvm_args: '--enable-preview' } + - { name: "26-ea", java_version_numeric: 26, from: 'jdk.java.net', jvm_args: '--enable-preview' } steps: - name: Checkout ${{ inputs.branch }} - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ inputs.branch }} - name: Get year/month for cache key @@ -198,7 +202,7 @@ jobs: echo "buildtool-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}-${CURRENT_DAY}" >> $GITHUB_OUTPUT - name: Cache Maven/Gradle Dependency/Dist Caches id: cache-maven - uses: actions/cache@v4 + uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 # if it's not a pull request, we restore and save the cache if: github.event_name != 'pull_request' with: @@ -215,7 +219,7 @@ jobs: ${{ steps.cache-key.outputs.buildtool-monthly-branch-cache-key }}- ${{ steps.cache-key.outputs.buildtool-monthly-cache-key }}- - name: Restore Maven/Gradle Dependency/Dist Caches - uses: actions/cache/restore@v4 + uses: actions/cache/restore@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 # if it's a pull request, we restore the cache, but we don't save it if: github.event_name == 'pull_request' with: @@ -231,13 +235,13 @@ jobs: - name: Set up latest JDK ${{ matrix.java.name }} from jdk.java.net if: matrix.java.from == 'jdk.java.net' - uses: oracle-actions/setup-java@v1 + uses: oracle-actions/setup-java@2e744f723b003fdd759727d0ff654c8717024845 # v1.4.0 with: website: jdk.java.net release: ${{ matrix.java.java_version_numeric }} - name: Set up latest JDK ${{ matrix.java.name }} from Adoptium if: matrix.java.from == '' || matrix.java.from == 'adoptium.net' - uses: actions/setup-java@v2.2.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: distribution: 'temurin' java-version: ${{ matrix.java.java_version_numeric }} @@ -247,7 +251,7 @@ jobs: run: echo "::set-output name=path::${JAVA_HOME}" # Always use JDK 11 to build the main code: that's what we use for releases. - name: Set up JDK 11 - uses: actions/setup-java@v2.2.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: distribution: 'temurin' java-version: 11 @@ -266,7 +270,7 @@ jobs: -Porg.gradle.java.installations.paths=${{ steps.mainjdk-exportpath.outputs.path }},${{ steps.testjdk-exportpath.outputs.path }} \ ${{ matrix.java.jvm_args && '-Ptest.jdk.launcher.args=' }}${{ matrix.java.jvm_args }} - name: Upload reports (if build failed) - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 if: failure() with: name: reports-java${{ matrix.java.name }} diff --git a/.gitignore b/.gitignore index 8016117ce..750319d47 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,3 @@ bin # Vim *.swp *.swo - -# Release scripts downloaded from hibernate/hibernate-release-scripts -.release diff --git a/.release/.gitignore b/.release/.gitignore new file mode 100644 index 000000000..cdd9a17d6 --- /dev/null +++ b/.release/.gitignore @@ -0,0 +1,3 @@ +# The folder into which we checkout our release scripts into +* +!.gitignore \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..3e99a1d42 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,107 @@ +# Contributing + +Contributions from the community are essential in keeping Hibernate (and any Open Source +project really) strong and successful. + +# Legal + +All original contributions to Hibernate are licensed under the +[Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0). + +The Apache 2.0 license text is included verbatim in the [LICENSE](LICENSE) file in the root directory +of the Hibernate Reactive repository. + +All contributions are subject to the [Developer Certificate of Origin (DCO)](https://developercertificate.org/). + +The DCO text is available verbatim in the [dco.txt](dco.txt) file in the root directory +of the Hibernate Reactive repository. + +## Guidelines + +While we try to keep requirements for contributing to a minimum, there are a few guidelines +we ask that you mind. + +For code contributions, these guidelines include: +* Respect the project code style - find templates for [IntelliJ IDEA](https://hibernate.org/community/contribute/intellij-idea/) or [Eclipse](https://hibernate.org/community/contribute/eclipse-ide/) +* Have a corresponding GitHub [issue](https://github.com/hibernate/hibernate-reactive/issues) and be sure to include + the key for this issue in your commit messages. +* Have a set of appropriate tests. + For your convenience, a [set of test templates](https://github.com/hibernate/hibernate-test-case-templates/tree/main/reactive) + have been made available. + + When submitting bug reports, the tests should reproduce the initially reported bug and illustrate that your solution addresses the issue. + For features/enhancements, the tests should demonstrate that the feature works as intended. + In both cases, be sure to incorporate your tests into the project to protect against possible regressions. +* If applicable, documentation should be updated to reflect the introduced changes +* The code compiles and the tests pass (`./gradlew clean build`) + +For documentation contributions, mainly to respect the project code style, especially in regard +to the use of tabs - as mentioned above, code style templates are available for both IntelliJ IDEA and Eclipse +IDEs. Ideally, these contributions would also have a corresponding issue, although this +is less necessary for documentation contributions. + +## Getting Started + +If you are just getting started with Git, GitHub, and/or contributing to Hibernate via +GitHub there are a few pre-requisite steps to follow: + +* Make sure you have a [GitHub account](https://github.com/signup/free) +* [Fork](https://help.github.com/articles/fork-a-repo) the Hibernate Reactive repository. As discussed in +the linked page, this also includes: + * [set up your local git install](https://help.github.com/articles/set-up-git) + * clone your fork +* Instruct git to ignore certain commits when using `git blame`. From the directory of your local clone, run this: `git config blame.ignoreRevsFile .git-blame-ignore-revs` +* See the wiki pages for setting up your IDE, whether you use +[IntelliJ IDEA](https://hibernate.org/community/contribute/intellij-idea/) +or [Eclipse](https://hibernate.org/community/contribute/eclipse-ide/)(1). + + +## Create the working (topic) branch + +Create a [topic branch](https://git-scm.com/book/en/Git-Branching-Branching-Workflows#Topic-Branches) +on which you will work. The convention is to incorporate the JIRA issue key in the name of this branch, +although this is more of a mnemonic strategy than a hard-and-fast rule - but doing so helps: +* Remember what each branch is for +* Isolate the work from other contributions you may be working on + +_If there is not already a GitHub issue covering the work you want to do, create one._ + +Assuming you will be working from the `main` branch and working +on the GitHub issue #123 : `git checkout -b 123 main` + +## Code + +Do your thing! + + +## Commit + +* Make commits of logical units +* Be sure to start each commit message using the ** GitHub issue key **. For example: + ``` + [#1234] Fix some kind of problem + ``` +* Make sure you have added the necessary tests for your changes +* Run _all_ the tests to ensure nothing else was accidentally broken + +_Before committing, if you want to pull in the latest upstream changes (highly +appreciated btw), please use rebasing rather than merging. Merging creates +"merge commits" that invariably muck up the project timeline._ + +## Submit + +* Push your changes to the topic branch in your fork of the repository +* Initiate a [pull request](https://help.github.com/articles/creating-a-pull-request) +* Adding the sentence `Fix #123`, where `#123` is the issue key, will link the pull request to the corresponding issue + and close it accordingly, when the pull request gets merged. + +It is important that this topic branch of your fork: + +* Is isolated to just the work on this one issue, or multiple issues if they are + related and also fixed/implemented by this work. The main point is to not push commits for more than + one PR to a single branch - GitHub PRs are linked to a branch rather than specific commits +* remain until the PR is closed. Once the underlying branch is deleted the corresponding PR will be closed, + if not already, and the changes will be lost. + +# Notes +(1) Gradle `eclipse` plugin is no longer supported, so the recommended way to import the project in your IDE is with the proper IDE tools/plugins. Don't try to run `./gradlew clean eclipse --refresh-dependencies` from the command line as you'll get an error because `eclipse` no longer exists diff --git a/README.md b/README.md index a44faa31b..6ba9fcc5c 100644 --- a/README.md +++ b/README.md @@ -29,22 +29,26 @@ Learn more at . Hibernate Reactive has been tested with: -- Java 11, 17, 21, 23 -- PostgreSQL 16 -- MySQL 8 +- Java 11, 17, 21, 24 +- PostgreSQL 17 +- MySQL 9 - MariaDB 11 -- Db2 11 +- Db2 12 - CockroachDB v24 - MS SQL Server 2022 - Oracle 23 -- [Hibernate ORM][] 6.6.3.Final -- [Vert.x Reactive PostgreSQL Client](https://vertx.io/docs/vertx-pg-client/java/) 4.5.11 -- [Vert.x Reactive MySQL Client](https://vertx.io/docs/vertx-mysql-client/java/) 4.5.11 -- [Vert.x Reactive Db2 Client](https://vertx.io/docs/vertx-db2-client/java/) 4.5.11 -- [Vert.x Reactive MS SQL Server Client](https://vertx.io/docs/vertx-mssql-client/java/) 4.5.11 -- [Vert.x Reactive Oracle Client](https://vertx.io/docs/vertx-oracle-client/java/) 4.5.11 +- [Hibernate ORM][] 6.6 +- [Vert.x Reactive PostgreSQL Client](https://vertx.io/docs/vertx-pg-client/java/) 4.5 +- [Vert.x Reactive MySQL Client](https://vertx.io/docs/vertx-mysql-client/java/) 4.5 +- [Vert.x Reactive Db2 Client](https://vertx.io/docs/vertx-db2-client/java/) 4.5 +- [Vert.x Reactive MS SQL Server Client](https://vertx.io/docs/vertx-mssql-client/java/) 4.5 +- [Vert.x Reactive Oracle Client](https://vertx.io/docs/vertx-oracle-client/java/) 4.5 - [Quarkus][Quarkus] via the Hibernate Reactive extension +The exact version of the libraries and images are in the +[catalog](https://github.com/hibernate/hibernate-reactive/blob/2.4/gradle/libs.versions.toml) +and in the [tooling/docker](https://github.com/hibernate/hibernate-reactive/tree/2.4/tooling/docker) folder. + [PostgreSQL]: https://www.postgresql.org [MySQL]: https://www.mysql.com [MariaDB]: https://mariadb.com diff --git a/build.gradle b/build.gradle index 5aee62970..9d43d4bca 100644 --- a/build.gradle +++ b/build.gradle @@ -3,54 +3,14 @@ plugins { id 'java-library' id 'maven-publish' - id 'com.diffplug.spotless' version '6.25.0' - id 'org.asciidoctor.jvm.convert' version '4.0.2' apply false - id 'io.github.gradle-nexus.publish-plugin' version '1.3.0' + alias(libs.plugins.com.diffplug.spotless) + alias(libs.plugins.org.asciidoctor.jvm.convert) apply false } group = "org.hibernate.reactive" // leverage the ProjectVersion which comes from the `local.versions` plugin version = project.projectVersion.fullName -ext { - if ( !project.hasProperty( 'hibernatePublishUsername' ) ) { - hibernatePublishUsername = null - } - if ( !project.hasProperty( 'hibernatePublishPassword' ) ) { - hibernatePublishPassword = null - } -} - -// Versions which need to be aligned across modules; this also -// allows overriding the build using a parameter, which can be -// useful to monitor compatibility for upcoming versions on CI: -// -// ./gradlew clean build -PhibernateOrmVersion=5.6.15-SNAPSHOT -ext { - // Mainly, to allow CI to test the latest versions of Vert.X - // Example: - // ./gradlew build -PvertxSqlClientVersion=4.0.0-SNAPSHOT - if ( !project.hasProperty( 'vertxSqlClientVersion' ) ) { - vertxSqlClientVersion = '4.5.11' - } - - testcontainersVersion = '1.20.4' - - logger.lifecycle "Vert.x SQL Client Version: " + project.vertxSqlClientVersion -} - -// To release, see task ciRelease in release/build.gradle -// To publish on Sonatype (Maven Central): -// ./gradlew publishToSonatype closeAndReleaseStagingRepository -PhibernatePublishUsername="" -PhibernatePublishPassword="" -nexusPublishing { - repositories { - sonatype { - username = project.hibernatePublishUsername - password = project.hibernatePublishPassword - } - } -} - subprojects { apply plugin: 'java-library' apply plugin: 'com.diffplug.spotless' @@ -77,9 +37,9 @@ subprojects { // Useful for local development, it should be disabled otherwise mavenLocal() } - // Example: ./gradlew build -PenableSonatypeOpenSourceSnapshotsRep - if ( project.hasProperty('enableSonatypeOpenSourceSnapshotsRep') ) { - maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } + // Example: ./gradlew build -PenableCentralSonatypeSnapshotsRep + if ( project.hasProperty('enableCentralSonatypeSnapshotsRep') ) { + maven { url 'https://central.sonatype.com/repository/maven-snapshots/' } } mavenCentral() @@ -91,6 +51,12 @@ subprojects { options.encoding = 'UTF-8' } + // Configure test tasks for all subprojects + tasks.withType( Test ).configureEach { + // Set the project root for finding Docker files - available to all modules + systemProperty 'hibernate.reactive.project.root', rootProject.projectDir.absolutePath + } + if ( !gradle.ext.javaToolchainEnabled ) { sourceCompatibility = JavaVersion.toVersion( gradle.ext.baselineJavaVersion ) targetCompatibility = JavaVersion.toVersion( gradle.ext.baselineJavaVersion ) @@ -156,3 +122,10 @@ subprojects { } } +rootProject.afterEvaluate { + // Workaround since "libs.versions.NAME" notation cannot be used here + def libs = project.extensions.getByType(VersionCatalogsExtension).named('libs') + logger.lifecycle "ORM version: ${libs.findVersion('hibernateOrmVersion').get().requiredVersion}" + logger.lifecycle "ORM Gradle plugin version: ${libs.findVersion('hibernateOrmGradlePluginVersion').get().requiredVersion}" + logger.lifecycle "Vert.x SQL Client version: ${libs.findVersion('vertxSqlClientVersion').get().requiredVersion}" +} diff --git a/ci/release/Jenkinsfile b/ci/release/Jenkinsfile index bb4315fb8..037d6eefb 100644 --- a/ci/release/Jenkinsfile +++ b/ci/release/Jenkinsfile @@ -1,9 +1,7 @@ #! /usr/bin/groovy /* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or . + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors */ /* @@ -17,11 +15,10 @@ import org.hibernate.jenkins.pipeline.helpers.version.Version // Global build configuration env.PROJECT = "reactive" env.JIRA_KEY = "HREACT" -def RELEASE_ON_PUSH = false // Set to `true` *only* on branches where you want a release on each push. +def RELEASE_ON_SCHEDULE = true // Set to `true` *only* on branches where you want a scheduled release. print "INFO: env.PROJECT = ${env.PROJECT}" print "INFO: env.JIRA_KEY = ${env.JIRA_KEY}" -print "INFO: RELEASE_ON_PUSH = ${RELEASE_ON_PUSH}" // -------------------------------------------- // Build conditions @@ -34,10 +31,17 @@ if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { } def manualRelease = currentBuild.getBuildCauses().toString().contains( 'UserIdCause' ) +def cronRelease = currentBuild.getBuildCauses().toString().contains( 'TimerTriggerCause' ) // Only do automatic release on branches where we opted in -if ( !manualRelease && !RELEASE_ON_PUSH ) { - print "INFO: Build skipped because automated releases are disabled on this branch. See constant RELEASE_ON_PUSH in ci/release/Jenkinsfile" +if ( !manualRelease && !cronRelease ) { + print "INFO: Build skipped because automated releases on push are disabled on this branch." + currentBuild.result = 'NOT_BUILT' + return +} + +if ( !manualRelease && cronRelease && !RELEASE_ON_SCHEDULE ) { + print "INFO: Build skipped because automated releases are disabled on this branch. See constant RELEASE_ON_SCHEDULE in ci/release/Jenkinsfile" currentBuild.result = 'NOT_BUILT' return } @@ -53,19 +57,23 @@ def checkoutReleaseScripts() { } } + // -------------------------------------------- // Pipeline pipeline { agent { - label 'Worker&&Containers' + label 'Release' + } + triggers { + // Run every week Sunday 1 AM + cron('0 1 * * 0') } tools { jdk 'OpenJDK 11 Latest' } options { buildDiscarder logRotator(daysToKeepStr: '30', numToKeepStr: '10') - rateLimitBuilds(throttle: [count: 1, durationName: 'day', userBoost: true]) disableConcurrentBuilds(abortPrevious: false) preserveStashes() } @@ -89,9 +97,13 @@ pipeline { ) } stages { - stage('Release check') { + stage('Check') { steps { script { + print "INFO: params.RELEASE_VERSION = ${params.RELEASE_VERSION}" + print "INFO: params.DEVELOPMENT_VERSION = ${params.DEVELOPMENT_VERSION}" + print "INFO: params.RELEASE_DRY_RUN? = ${params.RELEASE_DRY_RUN}" + checkoutReleaseScripts() def currentVersion = Version.parseDevelopmentVersion( sh( @@ -107,7 +119,9 @@ pipeline { echo "Release was requested manually" if ( !params.RELEASE_VERSION ) { - throw new IllegalArgumentException( 'Missing value for parameter RELEASE_VERSION. This parameter must be set explicitly to prevent mistakes.' ) + throw new IllegalArgumentException( + 'Missing value for parameter RELEASE_VERSION. This parameter must be set explicitly to prevent mistakes.' + ) } releaseVersion = Version.parseReleaseVersion( params.RELEASE_VERSION ) @@ -118,12 +132,15 @@ pipeline { else { echo "Release was triggered automatically" - // Avoid doing an automatic release for commits from a release - def lastCommitter = sh(script: 'git show -s --format=\'%an\'', returnStdout: true) - def secondLastCommitter = sh(script: 'git show -s --format=\'%an\' HEAD~1', returnStdout: true) - if (lastCommitter == 'Hibernate-CI' && secondLastCommitter == 'Hibernate-CI') { - print "INFO: Automatic release skipped because last commits were for the previous release" - currentBuild.result = 'ABORTED' + // Avoid doing an automatic release if there are no "releasable" commits since the last release (see release scripts for determination) + def releasableCommitCount = sh( + script: ".release/scripts/count-releasable-commits.sh ${env.PROJECT}", + returnStdout: true + ).trim().toInteger() + if ( releasableCommitCount <= 0 ) { + print "INFO: Automatic release skipped because no releasable commits were pushed since the previous release" + currentBuild.getRawBuild().getExecutor().interrupt(Result.NOT_BUILT) + sleep(1) // Interrupt is not blocking and does not take effect immediately. return } @@ -147,16 +164,38 @@ pipeline { env.RELEASE_VERSION = releaseVersion.toString() env.DEVELOPMENT_VERSION = developmentVersion.toString() - // Dry run is not supported at the moment - env.SCRIPT_OPTIONS = params.RELEASE_DRY_RUN ? "-d" : "" + env.SCRIPT_OPTIONS = params.RELEASE_DRY_RUN ? "-d" : "" + env.JRELEASER_DRY_RUN = params.RELEASE_DRY_RUN + } + } + } + stage('Prepare') { + steps { + script { + checkoutReleaseScripts() - // Determine version id to check if Jira version exists - // This step doesn't work for Hibernate Reactive (the project has been created with a different type on JIRA) - // sh ".release/scripts/determine-jira-version-id.sh ${env.JIRA_KEY} ${releaseVersion.withoutFinalQualifier}" + configFileProvider([ + configFile(fileId: 'release.config.ssh', targetLocation: "${env.HOME}/.ssh/config"), + configFile(fileId: 'release.config.ssh.knownhosts', targetLocation: "${env.HOME}/.ssh/known_hosts") + ]) { + sshagent(['ed25519.Hibernate-CI.github.com']) { + // set release version + // update changelog from JIRA + // tags the version + // changes the version to the provided development version + withEnv([ + "DISABLE_REMOTE_GRADLE_CACHE=true", + // Increase the amount of memory for this part since asciidoctor doc rendering consumes a lot of metaspace + "GRADLE_OPTS=-Dorg.gradle.jvmargs='-Dlog4j2.disableJmx -Xmx4g -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Duser.language=en -Duser.country=US -Duser.timezone=UTC -Dfile.encoding=UTF-8'" + ]) { + sh ".release/scripts/prepare-release.sh -j -b ${env.GIT_BRANCH} -v ${env.DEVELOPMENT_VERSION} ${env.PROJECT} ${env.RELEASE_VERSION}" + } + } + } } } } - stage('Release prepare') { + stage('Publish') { steps { script { checkoutReleaseScripts() @@ -166,22 +205,20 @@ pipeline { configFile(fileId: 'release.config.ssh.knownhosts', targetLocation: "${env.HOME}/.ssh/known_hosts") ]) { withCredentials([ - usernamePassword(credentialsId: 'ossrh.sonatype.org', passwordVariable: 'OSSRH_PASSWORD', usernameVariable: 'OSSRH_USER'), - usernamePassword(credentialsId: 'gradle-plugin-portal-api-key', passwordVariable: 'PLUGIN_PORTAL_PASSWORD', usernameVariable: 'PLUGIN_PORTAL_USERNAME'), - file(credentialsId: 'release.gpg.private-key', variable: 'RELEASE_GPG_PRIVATE_KEY_PATH'), - string(credentialsId: 'release.gpg.passphrase', variable: 'RELEASE_GPG_PASSPHRASE') + usernamePassword(credentialsId: 'central.sonatype.com', passwordVariable: 'JRELEASER_MAVENCENTRAL_TOKEN', usernameVariable: 'JRELEASER_MAVENCENTRAL_USERNAME'), + gitUsernamePassword(credentialsId: 'username-and-token.Hibernate-CI.github.com', gitToolName: 'Default'), + file(credentialsId: 'release.gpg.private-key', variable: 'RELEASE_GPG_PRIVATE_KEY_PATH'), + string(credentialsId: 'release.gpg.passphrase', variable: 'JRELEASER_GPG_PASSPHRASE'), + string(credentialsId: 'Hibernate-CI.github.com', variable: 'JRELEASER_GITHUB_TOKEN') ]) { - sshagent(['ed25519.Hibernate-CI.github.com', 'hibernate.filemgmt.jboss.org', 'hibernate-ci.frs.sourceforge.net']) { - // set release version - // update changelog from JIRA - // tags the version - // changes the version to the provided development version + sshagent(['ed25519.Hibernate-CI.github.com', 'jenkins.in.relation.to']) { + // performs documentation upload and Sonatype release + // push to github withEnv([ - "BRANCH=${env.GIT_BRANCH}", - // Increase the amount of memory for this part since asciidoctor doc rendering consumes a lot of metaspace - "GRADLE_OPTS=-Dorg.gradle.jvmargs='-Dlog4j2.disableJmx -Xmx4g -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Duser.language=en -Duser.country=US -Duser.timezone=UTC -Dfile.encoding=UTF-8'" + "DISABLE_REMOTE_GRADLE_CACHE=true" ]) { - sh ".release/scripts/prepare-release.sh ${env.PROJECT} ${env.RELEASE_VERSION} ${env.DEVELOPMENT_VERSION}" + def ghReleaseNote = sh('realpath -e github_release_notes.md 2>/dev/null', returnStdout: true).trim() + sh ".release/scripts/publish.sh -j ${ghReleaseNote != '' ? '--notes=' + ghReleaseNote : ''} ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION} ${env.DEVELOPMENT_VERSION} ${env.GIT_BRANCH}" } } } @@ -189,7 +226,7 @@ pipeline { } } } - stage('Publish release') { + stage('Update website') { steps { script { checkoutReleaseScripts() @@ -199,16 +236,17 @@ pipeline { configFile(fileId: 'release.config.ssh.knownhosts', targetLocation: "${env.HOME}/.ssh/known_hosts") ]) { withCredentials([ - usernamePassword(credentialsId: 'ossrh.sonatype.org', passwordVariable: 'OSSRH_PASSWORD', usernameVariable: 'OSSRH_USER'), - usernamePassword(credentialsId: 'gradle-plugin-portal-api-key', passwordVariable: 'PLUGIN_PORTAL_PASSWORD', usernameVariable: 'PLUGIN_PORTAL_USERNAME'), - file(credentialsId: 'release.gpg.private-key', variable: 'RELEASE_GPG_PRIVATE_KEY_PATH'), - string(credentialsId: 'release.gpg.passphrase', variable: 'RELEASE_GPG_PASSPHRASE'), - gitUsernamePassword(credentialsId: 'username-and-token.Hibernate-CI.github.com', gitToolName: 'Default') + gitUsernamePassword(credentialsId: 'username-and-token.Hibernate-CI.github.com', gitToolName: 'Default') ]) { - sshagent(['ed25519.Hibernate-CI.github.com', 'hibernate.filemgmt.jboss.org', 'hibernate-ci.frs.sourceforge.net']) { - // performs documentation upload and Sonatype release - // push to github - sh ".release/scripts/publish.sh ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION} ${env.DEVELOPMENT_VERSION} ${env.GIT_BRANCH}" + sshagent( ['ed25519.Hibernate-CI.github.com'] ) { + dir( '.release/hibernate.org' ) { + checkout scmGit( + branches: [[name: '*/production']], + extensions: [], + userRemoteConfigs: [[credentialsId: 'ed25519.Hibernate-CI.github.com', url: 'https://github.com/hibernate/hibernate.org.git']] + ) + sh "../scripts/website-release.sh ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION}" + } } } } @@ -223,4 +261,4 @@ pipeline { } } } -} \ No newline at end of file +} diff --git a/ci/snapshot-publish.Jenkinsfile b/ci/snapshot-publish.Jenkinsfile index c95f6663f..8af583e13 100644 --- a/ci/snapshot-publish.Jenkinsfile +++ b/ci/snapshot-publish.Jenkinsfile @@ -10,9 +10,17 @@ if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { return } +def checkoutReleaseScripts() { + dir('.release/scripts') { + checkout scmGit(branches: [[name: '*/main']], extensions: [], + userRemoteConfigs: [[credentialsId: 'ed25519.Hibernate-CI.github.com', + url: 'https://github.com/hibernate/hibernate-release-scripts.git']]) + } +} + pipeline { agent { - label 'Fedora' + label 'Release' } tools { jdk 'OpenJDK 11 Latest' @@ -30,16 +38,22 @@ pipeline { } stage('Publish') { steps { - withCredentials([ - usernamePassword(credentialsId: 'ossrh.sonatype.org', usernameVariable: 'hibernatePublishUsername', passwordVariable: 'hibernatePublishPassword'), - string(credentialsId: 'release.gpg.passphrase', variable: 'SIGNING_PASS'), - file(credentialsId: 'release.gpg.private-key', variable: 'SIGNING_KEYRING') - ]) { - sh '''./gradlew clean publish \ - -PhibernatePublishUsername=$hibernatePublishUsername \ - -PhibernatePublishPassword=$hibernatePublishPassword \ - --no-scan \ - ''' + script { + withCredentials([ + // TODO: Once we switch to maven-central publishing (from nexus2) we need to update credentialsId: + // https://docs.gradle.org/current/samples/sample_publishing_credentials.html#:~:text=via%20environment%20variables + usernamePassword(credentialsId: 'central.sonatype.com', passwordVariable: 'ORG_GRADLE_PROJECT_snapshotsPassword', usernameVariable: 'ORG_GRADLE_PROJECT_snapshotsUsername'), + gitUsernamePassword(credentialsId: 'username-and-token.Hibernate-CI.github.com', gitToolName: 'Default'), + string(credentialsId: 'Hibernate-CI.github.com', variable: 'JRELEASER_GITHUB_TOKEN') + ]) { + checkoutReleaseScripts() + def version = sh( + script: ".release/scripts/determine-current-version.sh reactive", + returnStdout: true + ).trim() + echo "Current version: '${version}'" + sh "bash -xe .release/scripts/snapshot-deploy.sh reactive ${version}" + } } } } @@ -51,4 +65,4 @@ pipeline { } } } -} \ No newline at end of file +} diff --git a/documentation/build.gradle b/documentation/build.gradle index aa6a27622..95c3dcb2b 100644 --- a/documentation/build.gradle +++ b/documentation/build.gradle @@ -52,7 +52,7 @@ def aggregateJavadocsTask = tasks.register( 'aggregateJavadocs', Javadoc ) { use = true options.encoding = 'UTF-8' - def matcher = hibernateOrmVersion =~ /\d+\.\d+/ + def matcher = libs.versions.hibernateOrmVersion =~ /\d+\.\d+/ def ormMinorVersion = matcher.find() ? matcher.group() : "5.6"; links = [ diff --git a/examples/native-sql-example/build.gradle b/examples/native-sql-example/build.gradle index fabdbc7fa..50c5a4117 100644 --- a/examples/native-sql-example/build.gradle +++ b/examples/native-sql-example/build.gradle @@ -8,9 +8,9 @@ buildscript { mavenLocal() } // Optional: Enables snapshots repository - // Example: ./gradlew build -PenableSonatypeOpenSourceSnapshotsRep - if ( project.hasProperty('enableSonatypeOpenSourceSnapshotsRep') ) { - maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } + // Example: ./gradlew build -PenableCentralSonatypeSnapshotsRep + if ( project.hasProperty('enableCentralSonatypeSnapshotsRep') ) { + maven { url 'https://central.sonatype.com/repository/maven-snapshots/' } } mavenCentral() } @@ -18,7 +18,7 @@ buildscript { plugins { // Optional: Hibernate Gradle plugin to enable bytecode enhancements - id "org.hibernate.orm" version "${hibernateOrmGradlePluginVersion}" + alias(libs.plugins.org.hibernate.orm) } description = 'Hibernate Reactive native SQL Example' @@ -27,20 +27,20 @@ dependencies { implementation project( ':hibernate-reactive-core' ) // Hibernate Validator (optional) - implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' - runtimeOnly 'org.glassfish.expressly:expressly:5.0.0' + implementation(libs.org.hibernate.validator.hibernate.validator) + runtimeOnly(libs.org.glassfish.expressly.expressly) // JPA metamodel generation for criteria queries (optional) - annotationProcessor "org.hibernate.orm:hibernate-jpamodelgen:${hibernateOrmVersion}" + annotationProcessor(libs.org.hibernate.orm.hibernate.jpamodelgen) // database driver for PostgreSQL - runtimeOnly "io.vertx:vertx-pg-client:${vertxSqlClientVersion}" + runtimeOnly(libs.io.vertx.vertx.pg.client) // logging (optional) - runtimeOnly "org.apache.logging.log4j:log4j-core:2.20.0" + runtimeOnly(libs.org.apache.logging.log4j.log4j.core) // Allow authentication to PostgreSQL using SCRAM: - runtimeOnly 'com.ongres.scram:client:2.1' + runtimeOnly(libs.com.ongres.scram.client) } // Optional: enable the bytecode enhancements @@ -75,3 +75,36 @@ tasks.register( "runAllExamples" ) { dependsOn = ["runAllExamplesOnPostgreSQL"] description = "Run all examples on ${dbs}" } + +// Optional: Task to print the resolved versions of Hibernate ORM and Vert.x +tasks.register( "printResolvedVersions" ) { + description = "Print the resolved hibernate-orm-core and vert.x versions" + doLast { + def hibernateCoreVersion = "n/a" + def vertxVersion = "n/a" + + // Resolve Hibernate Core and Vert.x versions from compile classpath + configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts.each { artifact -> + if (artifact.moduleVersion.id.name == 'hibernate-core') { + hibernateCoreVersion = artifact.moduleVersion.id.version + } + if (artifact.moduleVersion.id.group == 'io.vertx' && artifact.moduleVersion.id.name == 'vertx-sql-client') { + vertxVersion = artifact.moduleVersion.id.version + } + } + + // Print the resolved versions + println "Resolved Hibernate ORM Core Version: ${hibernateCoreVersion}" + println "Resolved Vert.x SQL client Version: ${vertxVersion}" + } +} + +// Make the version printing task run before tests and JavaExec tasks +tasks.withType( Test ).configureEach { + dependsOn printResolvedVersions +} + +tasks.withType( JavaExec ).configureEach { + dependsOn printResolvedVersions +} + diff --git a/examples/native-sql-example/src/main/resources/META-INF/persistence.xml b/examples/native-sql-example/src/main/resources/META-INF/persistence.xml index aaa9d3687..686442cb2 100644 --- a/examples/native-sql-example/src/main/resources/META-INF/persistence.xml +++ b/examples/native-sql-example/src/main/resources/META-INF/persistence.xml @@ -3,7 +3,7 @@ xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd" version="3.0"> - + org.hibernate.reactive.provider.ReactivePersistenceProvider org.hibernate.reactive.example.nativesql.Author diff --git a/examples/session-example/build.gradle b/examples/session-example/build.gradle index 7cf7fb5f6..41f9135a5 100644 --- a/examples/session-example/build.gradle +++ b/examples/session-example/build.gradle @@ -8,9 +8,9 @@ buildscript { mavenLocal() } // Optional: Enables snapshots repository - // Example: ./gradlew build -PenableSonatypeOpenSourceSnapshotsRep - if ( project.hasProperty('enableSonatypeOpenSourceSnapshotsRep') ) { - maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } + // Example: ./gradlew build -PenableCentralSonatypeSnapshotsRep + if ( project.hasProperty('enableCentralSonatypeSnapshotsRep') ) { + maven { url 'https://central.sonatype.com/repository/maven-snapshots/' } } mavenCentral() } @@ -18,7 +18,7 @@ buildscript { plugins { // Optional: Hibernate Gradle plugin to enable bytecode enhancements - id "org.hibernate.orm" version "${hibernateOrmGradlePluginVersion}" + alias(libs.plugins.org.hibernate.orm) } description = 'Hibernate Reactive Session Examples' @@ -27,21 +27,21 @@ dependencies { implementation project( ':hibernate-reactive-core' ) // Hibernate Validator (optional) - implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' - runtimeOnly 'org.glassfish.expressly:expressly:5.0.0' + implementation(libs.org.hibernate.validator.hibernate.validator) + runtimeOnly(libs.org.glassfish.expressly.expressly) // JPA metamodel generation for criteria queries (optional) - annotationProcessor "org.hibernate.orm:hibernate-jpamodelgen:${hibernateOrmVersion}" + annotationProcessor(libs.org.hibernate.orm.hibernate.jpamodelgen) // database drivers for PostgreSQL and MySQL - runtimeOnly "io.vertx:vertx-pg-client:${vertxSqlClientVersion}" - runtimeOnly "io.vertx:vertx-mysql-client:${vertxSqlClientVersion}" + runtimeOnly(libs.io.vertx.vertx.pg.client) + runtimeOnly(libs.io.vertx.vertx.mysql.client) // logging (optional) - runtimeOnly "org.apache.logging.log4j:log4j-core:2.20.0" + runtimeOnly(libs.org.apache.logging.log4j.log4j.core) // Allow authentication to PostgreSQL using SCRAM: - runtimeOnly 'com.ongres.scram:client:2.1' + runtimeOnly(libs.com.ongres.scram.client) } // Optional: enable the bytecode enhancements @@ -81,3 +81,35 @@ tasks.register( "runAllExamples" ) { dependsOn = ["runAllExamplesOnPostgreSQL", "runAllExamplesOnMySQL"] description = "Run all examples on ${dbs}" } + +// Optional: Task to print the resolved versions of Hibernate ORM and Vert.x +tasks.register( "printResolvedVersions" ) { + description = "Print the resolved hibernate-orm-core and vert.x versions" + doLast { + def hibernateCoreVersion = "n/a" + def vertxVersion = "n/a" + + // Resolve Hibernate Core and Vert.x versions from compile classpath + configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts.each { artifact -> + if (artifact.moduleVersion.id.name == 'hibernate-core') { + hibernateCoreVersion = artifact.moduleVersion.id.version + } + if (artifact.moduleVersion.id.group == 'io.vertx' && artifact.moduleVersion.id.name == 'vertx-sql-client') { + vertxVersion = artifact.moduleVersion.id.version + } + } + + // Print the resolved versions + println "Resolved Hibernate ORM Core Version: ${hibernateCoreVersion}" + println "Resolved Vert.x SQL client Version: ${vertxVersion}" + } +} + +// Make the version printing task run before tests and JavaExec tasks +tasks.withType( Test ).configureEach { + dependsOn printResolvedVersions +} + +tasks.withType( JavaExec ).configureEach { + dependsOn printResolvedVersions +} diff --git a/examples/session-example/src/main/resources/META-INF/persistence.xml b/examples/session-example/src/main/resources/META-INF/persistence.xml index 7c2c13900..f3271f251 100644 --- a/examples/session-example/src/main/resources/META-INF/persistence.xml +++ b/examples/session-example/src/main/resources/META-INF/persistence.xml @@ -3,7 +3,7 @@ xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd" version="3.0"> - + org.hibernate.reactive.provider.ReactivePersistenceProvider org.hibernate.reactive.example.session.Author diff --git a/github_release_notes.md b/github_release_notes.md new file mode 100644 index 000000000..db3469c5d --- /dev/null +++ b/github_release_notes.md @@ -0,0 +1,3 @@ + +* See the [website](https://hibernate.org/reactive/releases/{{releaseVersionFamily}}) for requirements and compatibilities. +* See the [What's New](https://hibernate.org/reactive/releases/{{releaseVersionFamily}}/#whats-new) guide for details about new features and capabilities. diff --git a/gradle.properties b/gradle.properties index 55ec51fc8..346e9fa42 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,9 @@ org.gradle.java.installations.auto-download=false ######################################################################### # Additional custom gradle build properties. -# Please, leave these properties commented upstream +# Please, leave these properties commented upstream. +# They are meant to be used for local builds or WIP branches. +# The same properties can be set from the command line. ########################################################################## # Enable Testcontainers + Docker when present (value ignored) @@ -28,28 +30,28 @@ org.gradle.java.installations.auto-download=false # Db2, MySql, PostgreSQL, CockroachDB, SqlServer, Oracle #db = MSSQL -# Enable the SonatypeOS maven repository (mainly for Vert.x snapshots) when present (value ignored) -#enableSonatypeOpenSourceSnapshotsRep = true +# Enable the maven Central Snapshot repository, when set to any value (the value is ignored) +#enableCentralSonatypeSnapshotsRep = true # Enable the maven local repository (for local development when needed) when present (value ignored) #enableMavenLocalRepo = true +### Settings the following properties will override the version defined in gradle/libs.versions.toml + # The default Hibernate ORM version (override using `-PhibernateOrmVersion=the.version.you.want`) -hibernateOrmVersion = 6.6.3.Final +#hibernateOrmVersion = 7.0.2.Final # Override default Hibernate ORM Gradle plugin version -# Using the stable version because I don't know how to configure the build to download the snapshot version from -# a remote repository -#hibernateOrmGradlePluginVersion = 6.6.3.Final +#hibernateOrmGradlePluginVersion = 7.0.2.Final # If set to true, skip Hibernate ORM version parsing (default is true, if set to null) # this is required when using intervals or weird versions or the build will fail #skipOrmVersionParsing = true # Override default Vert.x Sql client version -#vertxSqlClientVersion = 4.5.11-SNAPSHOT +#vertxSqlClientVersion = 4.5.16-SNAPSHOT # Override default Vert.x Web client and server versions. For integration tests, both default to vertxSqlClientVersion -#vertxWebVersion = 4.5.11 -#vertxWebtClientVersion = 4.5.11 +#vertxWebVersion = 4.5.16 +#vertxWebtClientVersion = 4.5.16 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..00dcf0fbb --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,60 @@ +[versions] +assertjVersion = "3.27.6" +hibernateOrmVersion = "6.6.34.Final" +hibernateOrmGradlePluginVersion = "6.6.34.Final" +jacksonDatabindVersion = "2.20.1" +jbossLoggingAnnotationVersion = "3.0.1.Final" +jbossLoggingVersion = "3.5.0.Final" +junitVersion = "5.13.3" +junitPlatformVersion = "1.13.3" +log4jVersion = "2.25.2" +testcontainersVersion = "1.21.3" +vertxSqlClientVersion = "4.5.22" +vertxWebVersion= "4.5.22" +vertxWebClientVersion = "4.5.22" + +[libraries] +com-fasterxml-jackson-core-jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jacksonDatabindVersion" } +com-ibm-db2-jcc = { group = "com.ibm.db2", name = "jcc", version = "12.1.2.0" } +com-microsoft-sqlserver-mssql-jdbc = { group = "com.microsoft.sqlserver", name = "mssql-jdbc", version = "13.2.1.jre11" } +com-mysql-mysql-connector-j = { group = "com.mysql", name = "mysql-connector-j", version = "9.5.0" } +com-ongres-scram-client = { group = "com.ongres.scram", name = "client", version = "2.1" } +io-smallrye-reactive-mutiny = { group = "io.smallrye.reactive", name = "mutiny", version = "2.7.0" } +io-vertx-vertx-db2-client = { group = "io.vertx", name = "vertx-db2-client", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-junit5 = { group = "io.vertx", name = "vertx-junit5", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-micrometer-metrics = { group = "io.vertx", name = "vertx-micrometer-metrics", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-mssql-client = { group = "io.vertx", name = "vertx-mssql-client", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-mysql-client = { group = "io.vertx", name = "vertx-mysql-client", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-oracle-client = { group = "io.vertx", name = "vertx-oracle-client", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-pg-client = { group = "io.vertx", name = "vertx-pg-client", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-sql-client = { group = "io.vertx", name = "vertx-sql-client", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-web = { group = "io.vertx", name = "vertx-web", version.ref = "vertxWebVersion" } +io-vertx-vertx-web-client = { group = "io.vertx", name = "vertx-web-client", version.ref = "vertxWebClientVersion" } +org-apache-logging-log4j-log4j-core = { group = "org.apache.logging.log4j", name = "log4j-core", version.ref = "log4jVersion" } +org-assertj-assertj-core = { group = "org.assertj", name = "assertj-core", version.ref = "assertjVersion" } +org-ehcache-ehcache = { group = "org.ehcache", name = "ehcache", version = "3.11.1" } +org-glassfish-expressly-expressly = { group = "org.glassfish.expressly", name = "expressly", version = "5.0.0" } +org-hibernate-orm-hibernate-core = { group = "org.hibernate.orm", name = "hibernate-core", version.ref = "hibernateOrmVersion" } +org-hibernate-orm-hibernate-jcache = { group = "org.hibernate.orm", name = "hibernate-jcache", version.ref = "hibernateOrmVersion" } +org-hibernate-orm-hibernate-jpamodelgen = { group = "org.hibernate.orm", name = "hibernate-jpamodelgen", version.ref = "hibernateOrmVersion" } +org-hibernate-validator-hibernate-validator = { group = "org.hibernate.validator", name = "hibernate-validator", version = "8.0.3.Final" } +org-jboss-logging-jboss-logging = { group = "org.jboss.logging", name = "jboss-logging", version.ref = "jbossLoggingVersion" } +org-jboss-logging-jboss-logging-annotations = { group = "org.jboss.logging", name = "jboss-logging-annotations", version.ref = "jbossLoggingAnnotationVersion" } +org-jboss-logging-jboss-logging-processor = { group = "org.jboss.logging", name = "jboss-logging-processor", version.ref = "jbossLoggingAnnotationVersion" } +org-junit-jupiter-junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junitVersion" } +org-junit-jupiter-junit-jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junitVersion" } +org-junit-platform-junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher", version = "junitPlatformVersion" } +org-mariadb-jdbc-mariadb-java-client = { group = "org.mariadb.jdbc", name = "mariadb-java-client", version = "3.5.6" } +org-postgresql-postgresql = { group = "org.postgresql", name = "postgresql", version = "42.7.8" } +org-testcontainers-cockroachdb = { group = "org.testcontainers", name = "cockroachdb", version.ref = "testcontainersVersion" } +org-testcontainers-db2 = { group = "org.testcontainers", name = "db2", version.ref = "testcontainersVersion" } +org-testcontainers-mariadb = { group = "org.testcontainers", name = "mariadb", version.ref = "testcontainersVersion" } +org-testcontainers-mssqlserver = { group = "org.testcontainers", name = "mssqlserver", version.ref = "testcontainersVersion" } +org-testcontainers-mysql = { group = "org.testcontainers", name = "mysql", version.ref = "testcontainersVersion" } +org-testcontainers-oracle-xe = { group = "org.testcontainers", name = "oracle-xe", version.ref = "testcontainersVersion" } +org-testcontainers-postgresql = { group = "org.testcontainers", name = "postgresql", version.ref = "testcontainersVersion" } + +[plugins] +com-diffplug-spotless = { id = "com.diffplug.spotless", version = "7.1.0" } +org-asciidoctor-jvm-convert = { id = "org.asciidoctor.jvm.convert", version = "4.0.5" } +org-hibernate-orm = { id = "org.hibernate.orm", version.ref = "hibernateOrmGradlePluginVersion" } diff --git a/gradle/version.properties b/gradle/version.properties index dd20a5fa4..85fe62918 100644 --- a/gradle/version.properties +++ b/gradle/version.properties @@ -1 +1 @@ -projectVersion=2.4.3-SNAPSHOT \ No newline at end of file +projectVersion=2.4.9-SNAPSHOT \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917..a4b76b953 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9355b4155..cea7a793a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a426..f5feea6d6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# 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/. @@ -84,7 +86,8 @@ done # 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 "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +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 diff --git a/gradlew.bat b/gradlew.bat index 6689b85be..9b42019c7 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @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 ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +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 @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +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 diff --git a/hibernate-reactive-core/build.gradle b/hibernate-reactive-core/build.gradle index bbcbc9e6c..ba8630386 100644 --- a/hibernate-reactive-core/build.gradle +++ b/hibernate-reactive-core/build.gradle @@ -8,76 +8,77 @@ apply from: publishScript dependencies { - api "org.hibernate.orm:hibernate-core:${hibernateOrmVersion}" + api(libs.org.hibernate.orm.hibernate.core) - api 'io.smallrye.reactive:mutiny:2.7.0' + api(libs.io.smallrye.reactive.mutiny) //Logging - implementation 'org.jboss.logging:jboss-logging:3.5.0.Final' - compileOnly 'org.jboss.logging:jboss-logging-annotations:2.2.1.Final' + implementation(libs.org.jboss.logging.jboss.logging) + compileOnly(libs.org.jboss.logging.jboss.logging.annotations) - annotationProcessor 'org.jboss.logging:jboss-logging:3.5.0.Final' - annotationProcessor 'org.jboss.logging:jboss-logging-annotations:2.2.1.Final' - annotationProcessor 'org.jboss.logging:jboss-logging-processor:2.2.1.Final' + annotationProcessor(libs.org.jboss.logging.jboss.logging) + annotationProcessor(libs.org.jboss.logging.jboss.logging.annotations) + annotationProcessor(libs.org.jboss.logging.jboss.logging.processor) //Specific implementation details of Hibernate Reactive: - implementation "io.vertx:vertx-sql-client:${vertxSqlClientVersion}" + implementation(libs.io.vertx.vertx.sql.client) // Testing - testImplementation 'org.assertj:assertj-core:3.26.3' - testImplementation "io.vertx:vertx-junit5:${vertxSqlClientVersion}" + testImplementation(libs.org.assertj.assertj.core) + testImplementation(libs.io.vertx.vertx.junit5) // Drivers - testImplementation "io.vertx:vertx-pg-client:${vertxSqlClientVersion}" - testImplementation "io.vertx:vertx-mysql-client:${vertxSqlClientVersion}" - testImplementation "io.vertx:vertx-db2-client:${vertxSqlClientVersion}" - testImplementation "io.vertx:vertx-mssql-client:${vertxSqlClientVersion}" - testImplementation "io.vertx:vertx-oracle-client:${vertxSqlClientVersion}" + testImplementation(libs.io.vertx.vertx.pg.client) + testImplementation(libs.io.vertx.vertx.mysql.client) + testImplementation(libs.io.vertx.vertx.db2.client) + testImplementation(libs.io.vertx.vertx.mssql.client) + testImplementation(libs.io.vertx.vertx.oracle.client) // Metrics - testImplementation "io.vertx:vertx-micrometer-metrics:${vertxSqlClientVersion}" + testImplementation(libs.io.vertx.vertx.micrometer.metrics) // Optional dependency of vertx-pg-client, essential when connecting via SASL SCRAM - testImplementation 'com.ongres.scram:client:2.1' + testImplementation(libs.com.ongres.scram.client) // JUnit Jupiter - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.3' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.11.3' + testImplementation(libs.org.junit.jupiter.junit.jupiter.api) + testRuntimeOnly(libs.org.junit.jupiter.junit.jupiter.engine) + testRuntimeOnly(libs.org.junit.platform.junit.platform.launcher) // JDBC driver to test with ORM and PostgreSQL - testRuntimeOnly "org.postgresql:postgresql:42.7.4" + testRuntimeOnly(libs.org.postgresql.postgresql) // JDBC driver for Testcontainers with MS SQL Server - testRuntimeOnly "com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11" + testRuntimeOnly(libs.com.microsoft.sqlserver.mssql.jdbc) // JDBC driver for Testcontainers with MariaDB Server - testRuntimeOnly "org.mariadb.jdbc:mariadb-java-client:3.5.1" + testRuntimeOnly(libs.org.mariadb.jdbc.mariadb.java.client) // JDBC driver for Testcontainers with MYSQL Server - testRuntimeOnly "com.mysql:mysql-connector-j:9.1.0" + testRuntimeOnly(libs.com.mysql.mysql.connector.j) // JDBC driver for Db2 server, for testing - testRuntimeOnly "com.ibm.db2:jcc:12.1.0.0" + testRuntimeOnly(libs.com.ibm.db2.jcc) // EHCache - testRuntimeOnly ("org.ehcache:ehcache:3.10.8") { + testRuntimeOnly(libs.org.ehcache.ehcache) { capabilities { requireCapability 'org.ehcache.modules:ehcache-xml-jakarta' } } - testRuntimeOnly ("org.hibernate.orm:hibernate-jcache:${hibernateOrmVersion}") + testRuntimeOnly(libs.org.hibernate.orm.hibernate.jcache) // log4j - testRuntimeOnly 'org.apache.logging.log4j:log4j-core:2.20.0' + testRuntimeOnly(libs.org.apache.logging.log4j.log4j.core) // Testcontainers - testImplementation "org.testcontainers:postgresql:${testcontainersVersion}" - testImplementation "org.testcontainers:mysql:${testcontainersVersion}" - testImplementation "org.testcontainers:mariadb:${testcontainersVersion}" - testImplementation "org.testcontainers:db2:${testcontainersVersion}" - testImplementation "org.testcontainers:cockroachdb:${testcontainersVersion}" - testImplementation "org.testcontainers:mssqlserver:${testcontainersVersion}" - testImplementation "org.testcontainers:oracle-xe:${testcontainersVersion}" + testImplementation(libs.org.testcontainers.postgresql) + testImplementation(libs.org.testcontainers.mysql) + testImplementation(libs.org.testcontainers.mariadb) + testImplementation(libs.org.testcontainers.db2) + testImplementation(libs.org.testcontainers.cockroachdb) + testImplementation(libs.org.testcontainers.mssqlserver) + testImplementation(libs.org.testcontainers.oracle.xe) } // Print a summary of the results of the tests (number of failures, successes and skipped) @@ -157,3 +158,35 @@ tasks.register( "testAll", Test ) { description = "Run tests for ${dbs}" dependsOn = dbs.collect( [] as HashSet ) { db -> "testDb${db}" } } + +// Task to print the resolved versions of Hibernate ORM and Vert.x +tasks.register( "printResolvedVersions" ) { + description = "Print the resolved hibernate-orm-core and vert.x versions" + doLast { + def hibernateCoreVersion = "n/a" + def vertxVersion = "n/a" + + // Resolve Hibernate Core and Vert.x versions from compile classpath + configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts.each { artifact -> + if (artifact.moduleVersion.id.name == 'hibernate-core') { + hibernateCoreVersion = artifact.moduleVersion.id.version + } + if (artifact.moduleVersion.id.group == 'io.vertx' && artifact.moduleVersion.id.name == 'vertx-sql-client') { + vertxVersion = artifact.moduleVersion.id.version + } + } + + // Print the resolved versions + println "Resolved Hibernate ORM Core Version: ${hibernateCoreVersion}" + println "Resolved Vert.x SQL client Version: ${vertxVersion}" + } +} + +// Make the version printing task run before tests and JavaExec tasks +tasks.withType( Test ).configureEach { + dependsOn printResolvedVersions +} + +tasks.withType( JavaExec ).configureEach { + dependsOn printResolvedVersions +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/CollectionTypes.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/CollectionTypes.java new file mode 100644 index 000000000..41dd0162b --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/CollectionTypes.java @@ -0,0 +1,490 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.engine.impl; + +import java.io.Serializable; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.CompletionStage; + +import org.hibernate.Hibernate; +import org.hibernate.HibernateException; +import org.hibernate.collection.spi.AbstractPersistentCollection; +import org.hibernate.collection.spi.PersistentArrayHolder; +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.spi.CollectionEntry; +import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.type.ArrayType; +import org.hibernate.type.CollectionType; +import org.hibernate.type.CustomCollectionType; +import org.hibernate.type.EntityType; +import org.hibernate.type.ForeignKeyDirection; +import org.hibernate.type.MapType; +import org.hibernate.type.Type; + +import static org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer.UNFETCHED_PROPERTY; +import static org.hibernate.internal.util.collections.CollectionHelper.mapOfSize; +import static org.hibernate.pretty.MessageHelper.collectionInfoString; +import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; +import static org.hibernate.reactive.util.impl.CompletionStages.loop; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; + +/** + * Reactive operations that really belong to {@link CollectionType} + * + */ +public class CollectionTypes { + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + /** + * @see org.hibernate.type.AbstractType#replace(Object, Object, SharedSessionContractImplementor, Object, Map, ForeignKeyDirection) + */ + public static CompletionStage replace( + CollectionType type, + Object original, + Object target, + SessionImplementor session, + Object owner, + Map copyCache, + ForeignKeyDirection foreignKeyDirection) + throws HibernateException { + // Collection and OneToOne are the only associations that could be TO_PARENT + return type.getForeignKeyDirection() == foreignKeyDirection + ? replace( type, original, target, session, owner, copyCache ) + : completedFuture( target ); + } + + /** + * @see CollectionType#replace(Object, Object, SharedSessionContractImplementor, Object, Map) + */ + public static CompletionStage replace( + CollectionType type, + Object original, + Object target, + SessionImplementor session, + Object owner, + Map copyCache) throws HibernateException { + if ( original == null ) { + return completedFuture( replaceNullOriginal( target, session ) ); + } + else if ( !Hibernate.isInitialized( original ) ) { + return completedFuture( replaceUninitializedOriginal( type, original, target, session, copyCache ) ); + } + else { + return replaceOriginal( type, original, target, session, owner, copyCache ); + } + } + + // todo: make org.hibernate.type.CollectionType#replaceNullOriginal public ? + /** + * @see CollectionType#replaceNullOriginal(Object, SharedSessionContractImplementor) + */ + private static Object replaceNullOriginal( + Object target, + SessionImplementor session) { + if ( target == null ) { + return null; + } + else if ( target instanceof Collection ) { + Collection collection = (Collection) target; + collection.clear(); + return collection; + } + else if ( target instanceof Map ) { + Map map = (Map) target; + map.clear(); + return map; + } + else { + final PersistenceContext persistenceContext = session.getPersistenceContext(); + final PersistentCollection collectionHolder = persistenceContext.getCollectionHolder( target ); + if ( collectionHolder != null ) { + if ( collectionHolder instanceof PersistentArrayHolder ) { + PersistentArrayHolder arrayHolder = (PersistentArrayHolder) collectionHolder; + persistenceContext.removeCollectionHolder( target ); + arrayHolder.beginRead(); + final PluralAttributeMapping attributeMapping = + persistenceContext.getCollectionEntry( collectionHolder ) + .getLoadedPersister().getAttributeMapping(); + arrayHolder.injectLoadedState( attributeMapping, null ); + arrayHolder.endRead(); + arrayHolder.dirty(); + persistenceContext.addCollectionHolder( collectionHolder ); + return arrayHolder.getArray(); + } + } + } + return null; + } + + // todo: make org.hibernate.type.CollectionType#replaceUninitializedOriginal public + private static Object replaceUninitializedOriginal( + CollectionType type, + Object original, + Object target, + SessionImplementor session, + Map copyCache) { + final PersistentCollection persistentCollection = (PersistentCollection) original; + if ( persistentCollection.hasQueuedOperations() ) { + if ( original == target ) { + // A managed entity with an uninitialized collection is being merged, + // We need to replace any detached entities in the queued operations + // with managed copies. + final AbstractPersistentCollection pc = (AbstractPersistentCollection) original; + pc.replaceQueuedOperationValues( + session.getFactory() + .getMappingMetamodel() + .getCollectionDescriptor( type.getRole() ), copyCache + ); + } + else { + // original is a detached copy of the collection; + // it contains queued operations, which will be ignored + LOG.ignoreQueuedOperationsOnMerge( + collectionInfoString( type.getRole(), persistentCollection.getKey() ) ); + } + } + return target; + } + + /** + * @see CollectionType#replaceOriginal(Object, Object, SharedSessionContractImplementor, Object, Map) + */ + private static CompletionStage replaceOriginal( + CollectionType type, + Object original, + Object target, + SessionImplementor session, + Object owner, + Map copyCache) { + + //for arrays, replaceElements() may return a different reference, since + //the array length might not match + return replaceElements( + type, + original, + instantiateResultIfNecessary( type, original, target ), + owner, + copyCache, + session + ).thenCompose( result -> { + if ( original == target ) { + // get the elements back into the target making sure to handle dirty flag + final boolean wasClean = + target instanceof PersistentCollection + && !( (PersistentCollection) target).isDirty(); + //TODO: this is a little inefficient, don't need to do a whole + // deep replaceElements() call + return replaceElements( type, result, target, owner, copyCache, session ) + .thenApply( unused -> { + if ( wasClean ) { + ( (PersistentCollection) target ).clearDirty(); + } + return target; + } ); + } + else { + return completedFuture( result ); + } + } ); + } + + /** + * @see CollectionType#replaceElements(Object, Object, Object, Map, SharedSessionContractImplementor) + */ + private static CompletionStage replaceElements( + CollectionType type, + Object original, + Object target, + Object owner, + Map copyCache, + SessionImplementor session) { + if ( type instanceof ArrayType ) { + return replaceArrayTypeElements( type, original, target, owner, copyCache, session ); + } + else if ( type instanceof CustomCollectionType ) { + return completedFuture( type.replaceElements( original, target, owner, copyCache, session ) ); + } + else if ( type instanceof MapType ) { + return replaceMapTypeElements( + type, + (Map) original, + (Map) target, + owner, + copyCache, + session + ); + } + else { + return replaceCollectionTypeElements( + type, + original, + (Collection) target, + owner, + copyCache, + session + ); + } + } + + private static CompletionStage replaceCollectionTypeElements( + CollectionType type, + Object original, + final Collection result, + Object owner, + Map copyCache, + SessionImplementor session) { + result.clear(); + + // copy elements into newly empty target collection + final Type elemType = type.getElementType( session.getFactory() ); + return loop( + (Collection) original, o -> getReplace( elemType, o, owner, session, copyCache ) + .thenAccept( result::add ) + ).thenCompose( unused -> { + // if the original is a PersistentCollection, and that original + // was not flagged as dirty, then reset the target's dirty flag + // here after the copy operation. + //

+ // One thing to be careful of here is a "bare" original collection + // in which case we should never ever ever reset the dirty flag + // on the target because we simply do not know... + if ( original instanceof PersistentCollection + && result instanceof PersistentCollection ) { + PersistentCollection resultPersistentCollection = (PersistentCollection) result; + PersistentCollection originalPersistentCollection = (PersistentCollection) original; + return preserveSnapshot( + originalPersistentCollection, resultPersistentCollection, + elemType, owner, copyCache, session + ).thenApply( v -> { + if ( !originalPersistentCollection.isDirty() ) { + resultPersistentCollection.clearDirty(); + } + return result; + } ); + } + else { + return completedFuture( result ); + } + } ); + } + + private static CompletionStage replaceMapTypeElements( + CollectionType type, + Map original, + Map target, + Object owner, + Map copyCache, + SessionImplementor session) { + final CollectionPersister persister = session.getFactory().getRuntimeMetamodels() + .getMappingMetamodel().getCollectionDescriptor( type.getRole() ); + final Map result = target; + result.clear(); + + return loop( + original.entrySet(), entry -> { + final Map.Entry me = entry; + return getReplace( persister.getIndexType(), me.getKey(), owner, session, copyCache ) + .thenCompose( key -> getReplace( + persister.getElementType(), + me.getValue(), + owner, + session, + copyCache + ).thenAccept( value -> result.put( key, value ) ) + ); + } + ).thenApply( unused -> result ); + } + + private static CompletionStage replaceArrayTypeElements( + CollectionType type, + Object original, + Object target, + Object owner, + Map copyCache, + SessionImplementor session) { + final Object result; + final int length = Array.getLength( original ); + if ( length != Array.getLength( target ) ) { + //note: this affects the return value! + result = ( (ArrayType) type ).instantiateResult( original ); + } + else { + result = target; + } + + final Type elemType = type.getElementType( session.getFactory() ); + return loop( + 0, length, i -> getReplace( elemType, Array.get( original, i ), owner, session, copyCache ) + .thenApply( o -> { + Array.set( result, i, o ); + return result; + } ) + ).thenApply( unused -> result ); + } + + private static CompletionStage getReplace( + Type elemType, + Object o, + Object owner, + SessionImplementor session, + Map copyCache) { + return getReplace( elemType, o, null, owner, session, copyCache ); + } + + private static CompletionStage getReplace( + Type elemType, + Object o, + Object target, + Object owner, + SessionImplementor session, + Map copyCache) { + if ( elemType instanceof EntityType ) { + return EntityTypes.replace( (EntityType) elemType, o, target, session, owner, copyCache ); + } + else { + final Object replace = elemType.replace( o, target, session, owner, copyCache ); + return completedFuture( replace ); + } + } + + /** + * @see CollectionType#preserveSnapshot(PersistentCollection, PersistentCollection, Type, Object, Map, SharedSessionContractImplementor) + */ + private static CompletionStage preserveSnapshot( + PersistentCollection original, + PersistentCollection result, + Type elemType, + Object owner, + Map copyCache, + SessionImplementor session) { + final CollectionEntry ce = session.getPersistenceContextInternal().getCollectionEntry( result ); + if ( ce != null ) { + return createSnapshot( original, result, elemType, owner, copyCache, session ) + .thenAccept( serializable -> ce.resetStoredSnapshot( result, serializable ) ); + } + return voidFuture(); + } + + /** + * @see CollectionType#createSnapshot(PersistentCollection, PersistentCollection, Type, Object, Map, SharedSessionContractImplementor) + */ + private static CompletionStage createSnapshot( + PersistentCollection original, + PersistentCollection result, + Type elemType, + Object owner, + Map copyCache, + SessionImplementor session) { + final Serializable originalSnapshot = original.getStoredSnapshot(); + if ( originalSnapshot instanceof List ) { + List list = (List) originalSnapshot; + return createListSnapshot( list, elemType, owner, copyCache, session ); + } + else if ( originalSnapshot instanceof Map ) { + Map map = (Map) originalSnapshot; + return createMapSnapshot( map, result, elemType, owner, copyCache, session ); + } + else if ( originalSnapshot instanceof Object[] ) { + Object[] array = (Object[]) originalSnapshot; + return createArraySnapshot( array, elemType, owner, copyCache, session ); + } + else { + // retain the same snapshot + return completedFuture( result.getStoredSnapshot() ); + } + } + + /** + * @see CollectionType#createArraySnapshot(Object[], Type, Object, Map, SharedSessionContractImplementor) + */ + private static CompletionStage createArraySnapshot( + Object[] array, + Type elemType, + Object owner, + Map copyCache, + SessionImplementor session) { + return loop( + 0, array.length, i -> getReplace( elemType, array[i], owner, session, copyCache ) + .thenAccept( o -> array[i] = o ) + ).thenApply( unused -> array ); + } + + /** + * @see CollectionType#createMapSnapshot(Map, PersistentCollection, Type, Object, Map, SharedSessionContractImplementor) + */ + private static CompletionStage createMapSnapshot( + Map map, + PersistentCollection result, + Type elemType, + Object owner, + Map copyCache, + SessionImplementor session) { + final Map resultSnapshot = (Map) result.getStoredSnapshot(); + final Map targetMap; + if ( map instanceof SortedMap ) { + SortedMap sortedMap = (SortedMap) map; + //noinspection unchecked, rawtypes + targetMap = new TreeMap( sortedMap.comparator() ); + } + else { + targetMap = mapOfSize( map.size() ); + } + return loop( + map.entrySet(), entry -> + getReplace( elemType, entry.getValue(), resultSnapshot, owner, session, copyCache ) + .thenAccept( newValue -> { + final Object key = entry.getKey(); + targetMap.put( key == entry.getValue() ? newValue : key, newValue ); + } ) + ).thenApply( v -> (Serializable) targetMap ); + } + + /** + * @see CollectionType#createListSnapshot(List, Type, Object, Map, SharedSessionContractImplementor) + */ + private static CompletionStage createListSnapshot( + List list, + Type elemType, + Object owner, + Map copyCache, + SessionImplementor session) { + final ArrayList targetList = new ArrayList<>( list.size() ); + return loop( + list, obj -> getReplace( elemType, obj, owner, session, copyCache ) + .thenAccept( targetList::add ) + ).thenApply( unused -> targetList ); + } + + /** + * @see CollectionType#instantiateResultIfNecessary(Object, Object) + */ + private static Object instantiateResultIfNecessary(CollectionType type, Object original, Object target) { + // for a null target, or a target which is the same as the original, + // we need to put the merged elements in a new collection + // by default just use an unanticipated capacity since we don't + // know how to extract the capacity to use from original here... + return target == null + || target == original + || target == UNFETCHED_PROPERTY + || target instanceof PersistentCollection && ( (PersistentCollection) target).isWrapper( original ) + ? type.instantiate( -1 ) + : target; + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/EntityTypes.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/EntityTypes.java index 879f7ddc8..fb1c29fe4 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/EntityTypes.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/EntityTypes.java @@ -25,6 +25,7 @@ import org.hibernate.reactive.persister.entity.impl.ReactiveEntityPersister; import org.hibernate.reactive.session.impl.ReactiveQueryExecutorLookup; import org.hibernate.reactive.session.impl.ReactiveSessionImpl; +import org.hibernate.type.CollectionType; import org.hibernate.type.EntityType; import org.hibernate.type.ForeignKeyDirection; import org.hibernate.type.OneToOneType; @@ -137,7 +138,7 @@ static CompletionStage loadByUniqueKey( else { return persister .reactiveLoadByUniqueKey( uniqueKeyPropertyName, key, session ) - .thenApply( ukResult -> loadHibernateProxyEntity( ukResult, session ) + .thenCompose( ukResult -> loadHibernateProxyEntity( ukResult, session ) .thenApply( targetUK -> { persistenceContext.addEntity( euk, targetUK ); return targetUK; @@ -158,42 +159,8 @@ public static CompletionStage replace( final Object owner, final Map copyCache) { Object[] copied = new Object[original.length]; - for ( int i = 0; i < types.length; i++ ) { - if ( original[i] == UNFETCHED_PROPERTY || original[i] == UNKNOWN ) { - copied[i] = target[i]; - } - else { - if ( !( types[i] instanceof EntityType ) ) { - copied[i] = types[i].replace( - original[i], - target[i] == UNFETCHED_PROPERTY ? null : target[i], - session, - owner, - copyCache - ); - } - } - } - return loop( 0, types.length, - i -> original[i] != UNFETCHED_PROPERTY && original[i] != UNKNOWN - && types[i] instanceof EntityType, - i -> replace( - (EntityType) types[i], - original[i], - target[i] == UNFETCHED_PROPERTY ? null : target[i], - session, - owner, - copyCache - ).thenCompose( copy -> { - if ( copy instanceof CompletionStage ) { - return ( (CompletionStage) copy ) - .thenAccept( nonStageCopy -> copied[i] = nonStageCopy ); - } - else { - copied[i] = copy; - return voidFuture(); - } - } ) + return loop( + 0, types.length, i -> replace( original, target, types, session, owner, copyCache, i, copied ) ).thenApply( v -> copied ); } @@ -209,43 +176,9 @@ public static CompletionStage replace( final Map copyCache, final ForeignKeyDirection foreignKeyDirection) { Object[] copied = new Object[original.length]; - for ( int i = 0; i < types.length; i++ ) { - if ( original[i] == UNFETCHED_PROPERTY || original[i] == UNKNOWN ) { - copied[i] = target[i]; - } - else { - if ( !( types[i] instanceof EntityType ) ) { - copied[i] = types[i].replace( - original[i], - target[i] == UNFETCHED_PROPERTY ? null : target[i], - session, - owner, - copyCache, - foreignKeyDirection - ); - } - } - } - return loop( 0, types.length, - i -> original[i] != UNFETCHED_PROPERTY && original[i] != UNKNOWN - && types[i] instanceof EntityType, - i -> replace( - (EntityType) types[i], - original[i], - target[i] == UNFETCHED_PROPERTY ? null : target[i], - session, - owner, - copyCache, - foreignKeyDirection - ).thenCompose( copy -> { - if ( copy instanceof CompletionStage ) { - return ( (CompletionStage) copy ).thenAccept( nonStageCopy -> copied[i] = nonStageCopy ); - } - else { - copied[i] = copy; - return voidFuture(); - } - } ) + return loop( + 0, types.length, + i -> replace( original, target, types, session, owner, copyCache, foreignKeyDirection, i, copied ) ).thenApply( v -> copied ); } @@ -272,7 +205,7 @@ private static CompletionStage replace( /** * @see EntityType#replace(Object, Object, SharedSessionContractImplementor, Object, Map) */ - private static CompletionStage replace( + protected static CompletionStage replace( EntityType entityType, Object original, Object target, @@ -336,15 +269,16 @@ private static CompletionStage resolveIdOrUniqueKey( // as a ComponentType. In the case that the entity is unfetched, we need to // explicitly fetch it here before calling replace(). (Note that in Hibernate // ORM this is unnecessary due to transparent lazy fetching.) - return ( (ReactiveSessionImpl) session ).reactiveFetch( id, true ) + return ( (ReactiveSessionImpl) session ) + .reactiveFetch( id, true ) .thenCompose( fetched -> { - Object idOrUniqueKey = entityType.getIdentifierOrUniqueKeyType( session.getFactory() ) + Object idOrUniqueKey = entityType + .getIdentifierOrUniqueKeyType( session.getFactory() ) .replace( fetched, null, session, owner, copyCache ); if ( idOrUniqueKey instanceof CompletionStage ) { return ( (CompletionStage) idOrUniqueKey ) .thenCompose( key -> resolve( entityType, key, owner, session ) ); } - return resolve( entityType, idOrUniqueKey, owner, session ); } ); } ); @@ -426,9 +360,9 @@ private static CompletionStage getIdentifierFromHibernateProxy( if ( type.isEntityIdentifierMapping() ) { propertyValue = getIdentifier( (EntityType) type, propertyValue, (SessionImplementor) session ); } - return completedFuture( propertyValue ); + return propertyValue; } - return nullFuture(); + return null; } ); } @@ -450,4 +384,100 @@ private static CompletionStage loadHibernateProxyEntity( } } + private static CompletionStage replace( + Object[] original, + Object[] target, + Type[] types, + SessionImplementor session, + Object owner, + Map copyCache, + int i, + Object[] copied) { + if ( original[i] == UNFETCHED_PROPERTY || original[i] == UNKNOWN ) { + copied[i] = target[i]; + return voidFuture(); + } + else if ( types[i] instanceof CollectionType ) { + return CollectionTypes.replace( + (CollectionType) types[i], + original[i], + target[i] == UNFETCHED_PROPERTY ? null : target[i], + session, + owner, + copyCache + ).thenAccept( copy -> copied[i] = copy ); + } + else if ( types[i] instanceof EntityType ) { + return replace( + (EntityType) types[i], + original[i], + target[i] == UNFETCHED_PROPERTY ? null : target[i], + session, + owner, + copyCache + ).thenAccept( copy -> copied[i] = copy ); + } + else { + final Type type = types[i]; + copied[i] = type.replace( + original[i], + target[i] == UNFETCHED_PROPERTY ? null : target[i], + session, + owner, + copyCache + ); + return voidFuture(); + } + } + + private static CompletionStage replace( + Object[] original, + Object[] target, + Type[] types, + SessionImplementor session, + Object owner, + Map copyCache, + ForeignKeyDirection foreignKeyDirection, + int i, + Object[] copied) { + if ( original[i] == UNFETCHED_PROPERTY || original[i] == UNKNOWN ) { + copied[i] = target[i]; + return voidFuture(); + } + else if ( types[i] instanceof CollectionType ) { + return CollectionTypes.replace( + (CollectionType) types[i], + original[i], + target[i] == UNFETCHED_PROPERTY ? null : target[i], + session, + owner, + copyCache, + foreignKeyDirection + ).thenAccept( copy -> copied[i] = copy ); + } + else if ( types[i] instanceof EntityType ) { + return replace( + (EntityType) types[i], + original[i], + target[i] == UNFETCHED_PROPERTY ? null : target[i], + session, + owner, + copyCache, + foreignKeyDirection + ).thenAccept( copy -> copied[i] = copy ); + } + else { + copied[i] = types[i].replace( + original[i], + target[i] == UNFETCHED_PROPERTY ? null : target[i], + session, + owner, + copyCache, + foreignKeyDirection + ); + return voidFuture(); + } + } + + } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveSaveEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveSaveEventListener.java index e9d68b00e..c0ef66b17 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveSaveEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveSaveEventListener.java @@ -144,55 +144,59 @@ else if ( generator instanceof Assigned ) { // the entity instance, so it will be available // to the entity in the @PrePersist callback if ( generator instanceof ReactiveIdentifierGenerator ) { - return ( (ReactiveIdentifierGenerator) generator ) - .generate( ( ReactiveConnectionSupplier ) source, entity ) - .thenApply( id -> castToIdentifierType( id, persister ) ) - .thenCompose( gid -> performSaveWithId( - entity, - context, - source, - persister, - generator, - gid, - requiresImmediateIdAccess, - false - ) ); + return generateId( entity, source, (ReactiveIdentifierGenerator) generator, persister ) + .thenCompose( gid -> { + if ( gid == SHORT_CIRCUIT_INDICATOR ) { + source.getIdentifier( entity ); + return voidFuture(); + } + persister.setIdentifier( entity, gid, source ); + return reactivePerformSave( + entity, + gid, + persister, + generatedOnExecution, + context, + source, + false + ); + } ); } generatedId = ( (BeforeExecutionGenerator) generator ).generate( source, entity, null, INSERT ); + if ( generatedId == SHORT_CIRCUIT_INDICATOR ) { + source.getIdentifier( entity ); + return voidFuture(); + } + persister.setIdentifier( entity, generatedId, source ); } final Object id = castToIdentifierType( generatedId, persister ); - return reactivePerformSave( entity, id, persister, generatedOnExecution, context, source, requiresImmediateIdAccess ); + final boolean delayIdentityInserts = !source.isTransactionInProgress() && !requiresImmediateIdAccess && generatedOnExecution; + return reactivePerformSave( entity, id, persister, generatedOnExecution, context, source, delayIdentityInserts ); } - private CompletionStage performSaveWithId( + private CompletionStage generateId( Object entity, - C context, EventSource source, - EntityPersister persister, - Generator generator, - Object generatedId, - boolean requiresImmediateIdAccess, - boolean generatedOnExecution) { - if ( generatedId == null ) { - throw new IdentifierGenerationException( "null id generated for: " + entity.getClass() ); - } - if ( generatedId == SHORT_CIRCUIT_INDICATOR ) { - source.getIdentifier( entity ); - return voidFuture(); - } - if ( LOG.isDebugEnabled() ) { - LOG.debugf( - "Generated identifier: %s, using strategy: %s", - persister.getIdentifierType().toLoggableString( generatedId, source.getFactory() ), - generator.getClass().getName() - ); - } - final boolean delayIdentityInserts = - !source.isTransactionInProgress() - && !requiresImmediateIdAccess - && generatedOnExecution; - return reactivePerformSave( entity, generatedId, persister, false, context, source, delayIdentityInserts ); + ReactiveIdentifierGenerator generator, + EntityPersister persister) { + return generator + .generate( (ReactiveConnectionSupplier) source, entity ) + .thenApply( id -> { + final Object generatedId = castToIdentifierType( id, persister ); + if ( generatedId == null ) { + throw new IdentifierGenerationException( "null id generated for: " + entity.getClass() ); + } + if ( LOG.isDebugEnabled() ) { + LOG.debugf( + "Generated identifier: %s, using strategy: %s", + persister.getIdentifierType().toLoggableString( generatedId, source.getFactory() ), + generator.getClass().getName() + ); + } + return generatedId; + } + ); } /** @@ -232,10 +236,7 @@ protected CompletionStage reactivePerformSave( if ( persister.getGenerator() instanceof Assigned ) { id = persister.getIdentifier( entity, source ); if ( id == null ) { - throw new IdentifierGenerationException( - "Identifier of entity '" + persister.getEntityName() - + "' must be manually assigned before calling 'persist()'" - ); + return failedFuture( new IdentifierGenerationException( "Identifier of entity '" + persister.getEntityName() + "' must be manually assigned before calling 'persist()'" ) ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveDeleteEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveDeleteEventListener.java index 66151f03b..6b2392e1e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveDeleteEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveDeleteEventListener.java @@ -48,6 +48,7 @@ import org.hibernate.reactive.event.ReactiveDeleteEventListener; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.session.ReactiveQueryProducer; import org.hibernate.reactive.session.ReactiveSession; import org.hibernate.type.CollectionType; import org.hibernate.type.CompositeType; @@ -199,7 +200,7 @@ private CompletionStage fetchAndDelete(DeleteEvent event, DeleteContext tr } //Object entity = persistenceContext.unproxyAndReassociate( event.getObject() ); - return ( (ReactiveSession) source ) + return ( (ReactiveQueryProducer) source ) .reactiveFetch( objectEvent, true ) .thenCompose( entity -> delete( event, transientEntities, entity ) ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLockEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLockEventListener.java index 7832298e0..a75ac1194 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLockEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLockEventListener.java @@ -35,6 +35,7 @@ import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.persister.entity.impl.ReactiveEntityPersister; +import org.hibernate.reactive.session.ReactiveQueryProducer; import org.hibernate.reactive.session.ReactiveSession; import static org.hibernate.pretty.MessageHelper.infoString; @@ -76,7 +77,7 @@ public CompletionStage reactiveOnLock(LockEvent event) throws HibernateExc //TODO: if object was an uninitialized proxy, this is inefficient, // resulting in two SQL selects - return ( (ReactiveSession) source ).reactiveFetch( event.getObject(), true ) + return ( (ReactiveQueryProducer) source ).reactiveFetch( event.getObject(), true ) .thenCompose( entity -> reactiveOnLock( event, entity ) ); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java index f5e5eb1b4..ba3edda4a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java @@ -37,7 +37,7 @@ import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.persister.entity.impl.ReactiveAbstractEntityPersister; -import org.hibernate.reactive.session.ReactiveSession; +import org.hibernate.reactive.session.ReactiveQueryProducer; import org.hibernate.type.CollectionType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; @@ -84,7 +84,7 @@ public CompletionStage reactiveOnRefresh(RefreshEvent event, RefreshContex // Hibernate Reactive doesn't support detached instances in refresh() throw new IllegalArgumentException( "Unmanaged instance passed to refresh()" ); } - return ( (ReactiveSession) source ) + return ( (ReactiveQueryProducer) source ) .reactiveFetch( event.getObject(), true ) .thenCompose( entity -> reactiveOnRefresh( event, refreshedAlready, entity ) ); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java index 72760a26b..03fd83457 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java @@ -7,6 +7,7 @@ +import java.sql.SQLException; import java.sql.SQLWarning; import jakarta.persistence.PersistenceException; @@ -32,8 +33,8 @@ public interface Log extends BasicLogger { @LogMessage(level = INFO) - @Message(id = 1, value = "Hibernate Reactive") - void startHibernateReactive(); + @Message(id = 1, value = "Hibernate Reactive version %s") + void startHibernateReactive(String version); @LogMessage(level = INFO) @Message(id = 2, value = "Vert.x not detected, creating a new instance") @@ -258,6 +259,13 @@ public interface Log extends BasicLogger { @Message(id = 80, value = "No results were returned by the query (you can try running it with '.executeUpdate()'): %1$s") HibernateException noResultException(String sql); + @Message(id = 84, value = "The application requested a JDBC connection, but Hibernate Reactive doesn't use JDBC. This could be caused by a bug or the use of an unsupported feature in Hibernate Reactive") + SQLException notUsingJdbc(); + + @LogMessage(level = ERROR) + @Message(id = 86, value = "Error closing reactive connection") + void errorClosingConnection(@Cause Throwable throwable); + // Same method that exists in CoreMessageLogger @LogMessage(level = WARN) @Message(id = 104, value = "firstResult/maxResults specified with collection fetch; applying in memory!" ) @@ -300,4 +308,8 @@ public interface Log extends BasicLogger { @LogMessage(level = WARN) @Message(id = 448, value = "Warnings creating temp table : %s") void warningsCreatingTempTable(SQLWarning warning); + + @LogMessage(level = WARN) + @Message( id= 494, value = "Attempt to merge an uninitialized collection with queued operations; queued operations will be ignored: %s") + void ignoreQueuedOperationsOnMerge(String collectionInfoString); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Version.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Version.java new file mode 100644 index 000000000..2ed6d6b9d --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Version.java @@ -0,0 +1,32 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.logging.impl; + +/** + * Information about the version of Hibernate Reactive. + * + * @author Steve Ebersole + */ +public final class Version { + + private static final String VERSION; + static { + final String version = Version.class.getPackage().getImplementationVersion(); + VERSION = version != null ? version : "[WORKING]"; + } + + private Version() { + } + + /** + * Access to the Hibernate Reactive version. + * + * @return The version + */ + public static String getVersionString() { + return VERSION; + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveEmbeddedIdentifierMappingImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveEmbeddedIdentifierMappingImpl.java index 3f5244fa9..204a23d3a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveEmbeddedIdentifierMappingImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveEmbeddedIdentifierMappingImpl.java @@ -13,6 +13,7 @@ import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.internal.EmbeddedIdentifierMappingImpl; +import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; import org.hibernate.reactive.sql.results.graph.embeddable.internal.ReactiveEmbeddableFetchImpl; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.SqlSelection; @@ -20,6 +21,7 @@ import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.Fetch; import org.hibernate.sql.results.graph.FetchParent; +import org.hibernate.sql.results.graph.Fetchable; public class ReactiveEmbeddedIdentifierMappingImpl extends AbstractCompositeIdentifierMapping { @@ -104,4 +106,13 @@ public String getSqlAliasStem() { public String getFetchableName() { return delegate.getFetchableName(); } + + @Override + public Fetchable getFetchable(int position) { + Fetchable fetchable = delegate.getFetchable( position ); + if ( fetchable instanceof ToOneAttributeMapping ) { + return new ReactiveToOneAttributeMapping( (ToOneAttributeMapping) fetchable ); + } + return fetchable; + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java index 65b0ad906..90972f9c5 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java @@ -2279,6 +2279,31 @@ default Uni withStatelessTransaction(Function> w */ Statistics getStatistics(); + /** + * Return the current instance of {@link Session}, if any. + * A current session exists only when this method is called + * from within an invocation of {@link #withSession(Function)} + * or {@link #withTransaction(Function)}. + * + * @return the current instance, if any, or {@code null} + * + * @since 2.4.7 + */ + Session getCurrentSession(); + + /** + * Return the current instance of {@link Session}, if any. + * A current session exists only when this method is called + * from within an invocation of + * {@link #withStatelessSession(Function)} or + * {@link #withStatelessTransaction(Function)}. + * + * @return the current instance, if any, or {@code null} + * + * @since 2.4.7 + */ + StatelessSession getCurrentStatelessSession(); + /** * Destroy the session factory and clean up its connection pool. */ diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/delegation/MutinySessionDelegator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/delegation/MutinySessionDelegator.java new file mode 100644 index 000000000..09d85913a --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/delegation/MutinySessionDelegator.java @@ -0,0 +1,341 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.mutiny.delegation; + +import io.smallrye.mutiny.Uni; +import jakarta.persistence.CacheRetrieveMode; +import jakarta.persistence.CacheStoreMode; +import jakarta.persistence.EntityGraph; +import jakarta.persistence.FlushModeType; +import jakarta.persistence.LockModeType; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.metamodel.Attribute; +import org.hibernate.CacheMode; +import org.hibernate.Filter; +import org.hibernate.FlushMode; +import org.hibernate.Incubating; +import org.hibernate.LockMode; +import org.hibernate.reactive.common.AffectedEntities; +import org.hibernate.reactive.common.Identifier; +import org.hibernate.reactive.common.ResultSetMapping; +import org.hibernate.reactive.mutiny.Mutiny; + +import java.util.List; +import java.util.function.Function; + +/** + * Wraps a {@linkplain #delegate} session. + * + * @author Gavin King + */ +public abstract class MutinySessionDelegator implements Mutiny.Session { + + public abstract Mutiny.Session delegate(); + + public Uni find(Class entityClass, Object id) { + return delegate().find(entityClass, id); + } + + public Uni find(Class entityClass, Object id, LockModeType lockModeType) { + return delegate().find(entityClass, id, lockModeType); + } + + public Uni find(Class entityClass, Object id, LockMode lockMode) { + return delegate().find(entityClass, id, lockMode); + } + + public Uni find(EntityGraph entityGraph, Object id) { + return delegate().find(entityGraph, id); + } + + public Uni> find(Class entityClass, Object... ids) { + return delegate().find(entityClass, ids); + } + + public Mutiny.SelectionQuery createNamedQuery(String queryName, Class resultType) { + return delegate().createNamedQuery(queryName, resultType); + } + + public Mutiny.SelectionQuery createQuery(String queryString, Class resultType) { + return delegate().createQuery(queryString, resultType); + } + + public boolean isReadOnly(Object entityOrProxy) { + return delegate().isReadOnly(entityOrProxy); + } + + public Mutiny.SelectionQuery createNativeQuery(String queryString, Class resultType, AffectedEntities affectedEntities) { + return delegate().createNativeQuery(queryString, resultType, affectedEntities); + } + + public boolean isDefaultReadOnly() { + return delegate().isDefaultReadOnly(); + } + + public Uni unproxy(T association) { + return delegate().unproxy(association); + } + + public Mutiny.MutationQuery createMutationQuery(String queryString) { + return delegate().createMutationQuery(queryString); + } + + public Uni close() { + return delegate().close(); + } + + public Mutiny.Session disableFetchProfile(String name) { + return delegate().disableFetchProfile(name); + } + + public EntityGraph getEntityGraph(Class rootType, String graphName) { + return delegate().getEntityGraph(rootType, graphName); + } + + public Mutiny.SelectionQuery createSelectionQuery(String queryString, Class resultType) { + return delegate().createSelectionQuery(queryString, resultType); + } + + public Uni refresh(Object entity, LockModeType lockModeType) { + return delegate().refresh(entity, lockModeType); + } + + public Uni lock(Object entity, LockModeType lockModeType) { + return delegate().lock(entity, lockModeType); + } + + public Mutiny.SelectionQuery createNativeQuery(String queryString, ResultSetMapping resultSetMapping, AffectedEntities affectedEntities) { + return delegate().createNativeQuery(queryString, resultSetMapping, affectedEntities); + } + + public Uni lock(Object entity, LockMode lockMode) { + return delegate().lock(entity, lockMode); + } + + @Incubating + public Uni find(Class entityClass, Identifier naturalId) { + return delegate().find(entityClass, naturalId); + } + + public Uni withTransaction(Function> work) { + return delegate().withTransaction(work); + } + + public Mutiny.SelectionQuery createNativeQuery(String queryString, ResultSetMapping resultSetMapping) { + return delegate().createNativeQuery(queryString, resultSetMapping); + } + + public EntityGraph createEntityGraph(Class rootType, String graphName) { + return delegate().createEntityGraph(rootType, graphName); + } + + public Mutiny.Transaction currentTransaction() { + return delegate().currentTransaction(); + } + + public Mutiny.Session detach(Object entity) { + return delegate().detach(entity); + } + + public Mutiny.Session setCacheStoreMode(CacheStoreMode cacheStoreMode) { + return delegate().setCacheStoreMode(cacheStoreMode); + } + + public FlushMode getFlushMode() { + return delegate().getFlushMode(); + } + + public LockMode getLockMode(Object entity) { + return delegate().getLockMode(entity); + } + + public Mutiny.Query createNamedQuery(String queryName) { + return delegate().createNamedQuery(queryName); + } + + public Mutiny.SessionFactory getFactory() { + return delegate().getFactory(); + } + + public Mutiny.SelectionQuery createNativeQuery(String queryString, Class resultType) { + return delegate().createNativeQuery(queryString, resultType); + } + + public Mutiny.Session setSubselectFetchingEnabled(boolean enabled) { + return delegate().setSubselectFetchingEnabled(enabled); + } + + public Mutiny.Session setFlushMode(FlushMode flushMode) { + return delegate().setFlushMode(flushMode); + } + + public Uni remove(Object entity) { + return delegate().remove(entity); + } + + public Mutiny.Session setCacheMode(CacheMode cacheMode) { + return delegate().setCacheMode(cacheMode); + } + + public Filter enableFilter(String filterName) { + return delegate().enableFilter(filterName); + } + + public Mutiny.Query createNativeQuery(String queryString, AffectedEntities affectedEntities) { + return delegate().createNativeQuery(queryString, affectedEntities); + } + + public Mutiny.Session setReadOnly(Object entityOrProxy, boolean readOnly) { + return delegate().setReadOnly(entityOrProxy, readOnly); + } + + public EntityGraph createEntityGraph(Class rootType) { + return delegate().createEntityGraph(rootType); + } + + public Uni refreshAll(Object... entities) { + return delegate().refreshAll(entities); + } + + public Integer getBatchSize() { + return delegate().getBatchSize(); + } + + public Uni refresh(Object entity, LockMode lockMode) { + return delegate().refresh(entity, lockMode); + } + + public T getReference(Class entityClass, Object id) { + return delegate().getReference(entityClass, id); + } + + public T getReference(T entity) { + return delegate().getReference(entity); + } + + public Mutiny.Session setBatchSize(Integer batchSize) { + return delegate().setBatchSize(batchSize); + } + + public Uni refresh(Object entity) { + return delegate().refresh(entity); + } + + public CacheMode getCacheMode() { + return delegate().getCacheMode(); + } + + public Uni mergeAll(Object... entities) { + return delegate().mergeAll(entities); + } + + public Uni persist(Object object) { + return delegate().persist(object); + } + + public boolean contains(Object entity) { + return delegate().contains(entity); + } + + public int getFetchBatchSize() { + return delegate().getFetchBatchSize(); + } + + public Mutiny.Session setDefaultReadOnly(boolean readOnly) { + return delegate().setDefaultReadOnly(readOnly); + } + + public Mutiny.Session clear() { + return delegate().clear(); + } + + public Uni fetch(E entity, Attribute field) { + return delegate().fetch(entity, field); + } + + public Mutiny.SelectionQuery createQuery(CriteriaQuery criteriaQuery) { + return delegate().createQuery(criteriaQuery); + } + + public Mutiny.Session setCacheRetrieveMode(CacheRetrieveMode cacheRetrieveMode) { + return delegate().setCacheRetrieveMode(cacheRetrieveMode); + } + + public Uni removeAll(Object... entities) { + return delegate().removeAll(entities); + } + + public Filter getEnabledFilter(String filterName) { + return delegate().getEnabledFilter(filterName); + } + + public void disableFilter(String filterName) { + delegate().disableFilter(filterName); + } + + public Mutiny.MutationQuery createQuery(CriteriaDelete criteriaDelete) { + return delegate().createQuery(criteriaDelete); + } + + public Mutiny.Session enableFetchProfile(String name) { + return delegate().enableFetchProfile(name); + } + + public ResultSetMapping getResultSetMapping(Class resultType, String mappingName) { + return delegate().getResultSetMapping(resultType, mappingName); + } + + public Uni flush() { + return delegate().flush(); + } + + public Mutiny.Query createNativeQuery(String queryString) { + return delegate().createNativeQuery(queryString); + } + + public boolean isFetchProfileEnabled(String name) { + return delegate().isFetchProfileEnabled(name); + } + + public Uni merge(T entity) { + return delegate().merge(entity); + } + + public boolean isSubselectFetchingEnabled() { + return delegate().isSubselectFetchingEnabled(); + } + + public Mutiny.MutationQuery createQuery(CriteriaUpdate criteriaUpdate) { + return delegate().createQuery(criteriaUpdate); + } + + public Mutiny.Session setFetchBatchSize(int batchSize) { + return delegate().setFetchBatchSize(batchSize); + } + + public Uni fetch(T association) { + return delegate().fetch(association); + } + + public Uni persistAll(Object... entities) { + return delegate().persistAll(entities); + } + + @Deprecated + public Mutiny.Query createQuery(String queryString) { + return delegate().createQuery(queryString); + } + + public Mutiny.Session setFlushMode(FlushModeType flushModeType) { + return delegate().setFlushMode(flushModeType); + } + + public boolean isOpen() { + return delegate().isOpen(); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/delegation/MutinyStatelessSessionDelegator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/delegation/MutinyStatelessSessionDelegator.java new file mode 100644 index 000000000..152140006 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/delegation/MutinyStatelessSessionDelegator.java @@ -0,0 +1,191 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.mutiny.delegation; + +import io.smallrye.mutiny.Uni; +import jakarta.persistence.EntityGraph; +import jakarta.persistence.LockModeType; +import jakarta.persistence.criteria.CriteriaQuery; +import org.hibernate.Incubating; +import org.hibernate.LockMode; +import org.hibernate.reactive.common.ResultSetMapping; +import org.hibernate.reactive.mutiny.Mutiny; + +import java.util.function.Function; + +/** + * Wraps a {@linkplain #delegate} stateless session. + * + * @author Gavin King + */ +public abstract class MutinyStatelessSessionDelegator implements Mutiny.StatelessSession { + + public abstract Mutiny.StatelessSession delegate(); + + public Uni get(Class entityClass, Object id) { + return delegate().get(entityClass, id); + } + + @Deprecated + public Mutiny.Query createQuery(String queryString) { + return delegate().createQuery(queryString); + } + + public Uni get(EntityGraph entityGraph, Object id) { + return delegate().get(entityGraph, id); + } + + public Mutiny.SelectionQuery createNativeQuery(String queryString, ResultSetMapping resultSetMapping) { + return delegate().createNativeQuery(queryString, resultSetMapping); + } + + public Uni get(Class entityClass, Object id, LockMode lockMode) { + return delegate().get(entityClass, id, lockMode); + } + + public boolean isOpen() { + return delegate().isOpen(); + } + + public Uni insertAll(Object... entities) { + return delegate().insertAll(entities); + } + + public Uni updateAll(int batchSize, Object... entities) { + return delegate().updateAll(batchSize, entities); + } + + public EntityGraph createEntityGraph(Class rootType, String graphName) { + return delegate().createEntityGraph(rootType, graphName); + } + + public EntityGraph getEntityGraph(Class rootType, String graphName) { + return delegate().getEntityGraph(rootType, graphName); + } + + public Uni get(Class entityClass, Object id, LockModeType lockModeType) { + return delegate().get(entityClass, id, lockModeType); + } + + public Uni update(Object entity) { + return delegate().update(entity); + } + + public Uni refreshAll(int batchSize, Object... entities) { + return delegate().refreshAll(batchSize, entities); + } + + public Mutiny.SelectionQuery createQuery(String queryString, Class resultType) { + return delegate().createQuery(queryString, resultType); + } + + public Uni delete(Object entity) { + return delegate().delete(entity); + } + + public Uni refresh(Object entity, LockModeType lockModeType) { + return delegate().refresh(entity, lockModeType); + } + + public Mutiny.SessionFactory getFactory() { + return delegate().getFactory(); + } + + public Mutiny.SelectionQuery createNativeQuery(String queryString, Class resultType) { + return delegate().createNativeQuery(queryString, resultType); + } + + public Uni deleteAll(Object... entities) { + return delegate().deleteAll(entities); + } + + public Mutiny.SelectionQuery createSelectionQuery(String queryString, Class resultType) { + return delegate().createSelectionQuery(queryString, resultType); + } + + @Incubating + public Uni upsert(Object entity) { + return delegate().upsert(entity); + } + + public Mutiny.MutationQuery createMutationQuery(String queryString) { + return delegate().createMutationQuery(queryString); + } + + public Uni refresh(Object entity) { + return delegate().refresh(entity); + } + + public ResultSetMapping getResultSetMapping(Class resultType, String mappingName) { + return delegate().getResultSetMapping(resultType, mappingName); + } + + public Uni insertAll(int batchSize, Object... entities) { + return delegate().insertAll(batchSize, entities); + } + + public Mutiny.Query createNativeQuery(String queryString) { + return delegate().createNativeQuery(queryString); + } + + public Mutiny.Query createNamedQuery(String queryName) { + return delegate().createNamedQuery(queryName); + } + + public Mutiny.SelectionQuery createNamedQuery(String queryName, Class resultType) { + return delegate().createNamedQuery(queryName, resultType); + } + + public Uni refreshAll(Object... entities) { + return delegate().refreshAll(entities); + } + + public Uni close() { + return delegate().close(); + } + + public Uni updateAll(Object... entities) { + return delegate().updateAll(entities); + } + + public Mutiny.SelectionQuery createQuery(CriteriaQuery criteriaQuery) { + return delegate().createQuery(criteriaQuery); + } + + public Uni withTransaction(Function> work) { + return delegate().withTransaction(work); + } + + public Uni fetch(T association) { + return delegate().fetch(association); + } + + @Incubating + public Uni upsert(String entityName, Object entity) { + return delegate().upsert(entityName, entity); + } + + @Incubating + public Mutiny.Transaction currentTransaction() { + return delegate().currentTransaction(); + } + + public EntityGraph createEntityGraph(Class rootType) { + return delegate().createEntityGraph(rootType); + } + + public Uni insert(Object entity) { + return delegate().insert(entity); + } + + public Uni refresh(Object entity, LockMode lockMode) { + return delegate().refresh(entity, lockMode); + } + + public Uni deleteAll(int batchSize, Object... entities) { + return delegate().deleteAll(batchSize, entities); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionFactoryImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionFactoryImpl.java index aecc99e37..23feaba57 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionFactoryImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionFactoryImpl.java @@ -148,6 +148,16 @@ private CompletionStage connection(String tenantId) { : connectionPool.getConnection( tenantId ); } + @Override + public Mutiny.Session getCurrentSession() { + return context.get( contextKeyForSession ); + } + + @Override + public Mutiny.StatelessSession getCurrentStatelessSession() { + return context.get( contextKeyForStatelessSession ); + } + @Override public Uni withSession(Function> work) { Objects.requireNonNull( work, "parameter 'work' is required" ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/collection/mutation/ReactiveUpdateRowsCoordinatorOneToMany.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/collection/mutation/ReactiveUpdateRowsCoordinatorOneToMany.java index 99bec6f5a..8b7478efa 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/collection/mutation/ReactiveUpdateRowsCoordinatorOneToMany.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/collection/mutation/ReactiveUpdateRowsCoordinatorOneToMany.java @@ -7,6 +7,7 @@ import java.util.Iterator; import java.util.concurrent.CompletionStage; +import java.util.function.Function; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; @@ -28,9 +29,9 @@ import static java.lang.invoke.MethodHandles.lookup; import static org.hibernate.reactive.logging.impl.LoggerFactory.make; -import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; import static org.hibernate.reactive.util.impl.CompletionStages.loop; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; +import static org.hibernate.reactive.util.impl.CompletionStages.zeroFuture; import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER; import static org.hibernate.sql.model.MutationType.DELETE; import static org.hibernate.sql.model.MutationType.INSERT; @@ -70,15 +71,18 @@ public CompletionStage reactiveUpdateRows(Object key, PersistentCollection } private CompletionStage doReactiveUpdate(Object key, PersistentCollection collection, SharedSessionContractImplementor session) { - if ( rowMutationOperations.hasDeleteRow() ) { - deleteRows( key, collection, session ); - } + final Function> insertRowsFun = v -> { + if ( rowMutationOperations.hasInsertRow() ) { + return insertRows( key, collection, session ); + } - if ( rowMutationOperations.hasInsertRow() ) { - return insertRows( key, collection, session ); + return zeroFuture(); + }; + if ( rowMutationOperations.hasDeleteRow() ) { + return deleteRows( key, collection, session ) + .thenCompose( insertRowsFun ); } - - return completedFuture( 0 ); + return insertRowsFun.apply( null ); } private CompletionStage insertRows(Object key, PersistentCollection collection, SharedSessionContractImplementor session) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveUpdateCoordinatorStandard.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveUpdateCoordinatorStandard.java index d3237cf72..f0aa8bdc9 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveUpdateCoordinatorStandard.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveUpdateCoordinatorStandard.java @@ -31,7 +31,7 @@ import org.hibernate.tuple.entity.EntityMetamodel; import static org.hibernate.engine.jdbc.mutation.internal.ModelMutationHelper.identifiedResultsCheck; -import static org.hibernate.generator.EventType.INSERT; +import static org.hibernate.generator.EventType.UPDATE; import static org.hibernate.internal.util.collections.ArrayHelper.EMPTY_INT_ARRAY; import static org.hibernate.internal.util.collections.ArrayHelper.trim; import static org.hibernate.reactive.persister.entity.mutation.GeneratorValueUtil.generateValue; @@ -194,7 +194,7 @@ private CompletionStage reactivePreUpdateInMemoryValueGeneration( && generator.generatesOnUpdate() ) { final Object currentValue = currentValues[i]; final BeforeExecutionGenerator beforeGenerator = (BeforeExecutionGenerator) generator; - result = result.thenCompose( v -> generateValue( session, entity, currentValue, beforeGenerator, INSERT ) + result = result.thenCompose( v -> generateValue( session, entity, currentValue, beforeGenerator, UPDATE ) .thenAccept( generatedValue -> { currentValues[index] = generatedValue; entityPersister().setValue( entity, index, generatedValue ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/impl/ReactiveIntegrator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/impl/ReactiveIntegrator.java index 0d1876320..cda361d5a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/impl/ReactiveIntegrator.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/impl/ReactiveIntegrator.java @@ -28,6 +28,7 @@ import org.hibernate.reactive.event.impl.DefaultReactiveResolveNaturalIdEventListener; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.logging.impl.Version; import org.hibernate.service.ServiceRegistry; import org.hibernate.service.spi.SessionFactoryServiceRegistry; @@ -54,7 +55,7 @@ public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactor private void attachEventContextManagingListenersIfRequired(ServiceRegistry serviceRegistry) { if ( ReactiveModeCheck.isReactiveRegistry( serviceRegistry ) ) { - LOG.startHibernateReactive(); + LOG.startHibernateReactive( Version.getVersionString() ); EventListenerRegistry eventListenerRegistry = serviceRegistry.getService( EventListenerRegistry.class ); eventListenerRegistry.addDuplicationStrategy( ReplacementDuplicationStrategy.INSTANCE ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcConnectionProvider.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcConnectionProvider.java index 5c3380f65..a02c3e152 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcConnectionProvider.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcConnectionProvider.java @@ -6,10 +6,14 @@ package org.hibernate.reactive.provider.service; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; +import org.hibernate.reactive.logging.impl.Log; import java.sql.Connection; import java.sql.SQLException; +import static java.lang.invoke.MethodHandles.lookup; +import static org.hibernate.reactive.logging.impl.LoggerFactory.make; + /** * A dummy Hibernate {@link ConnectionProvider} throws an * exception if a JDBC connection is requested. @@ -17,12 +21,13 @@ * @author Gavin King */ public class NoJdbcConnectionProvider implements ConnectionProvider { + private static final Log LOG = make( Log.class, lookup() ); public static final NoJdbcConnectionProvider INSTANCE = new NoJdbcConnectionProvider(); @Override public Connection getConnection() throws SQLException { - throw new SQLException( "Not using JDBC" ); + throw LOG.notUsingJdbc(); } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/OracleSqlReactiveInformationExtractorImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/OracleSqlReactiveInformationExtractorImpl.java index 67852da79..21f2a0dc1 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/OracleSqlReactiveInformationExtractorImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/OracleSqlReactiveInformationExtractorImpl.java @@ -185,12 +185,45 @@ protected String getResultSetIsNullableLabel() { @Override protected int dataTypeCode(String typeName) { - // ORACLE only supports "float" sql type for double precision - // so return code for double for both double and float column types - if ( typeName.equalsIgnoreCase( "float" ) || - typeName.toLowerCase().startsWith( "double" ) ) { + if ( typeName.equalsIgnoreCase( "float" ) + || typeName.toLowerCase().startsWith( "double" ) + || typeName.equalsIgnoreCase( "binary_double" ) ) { return Types.DOUBLE; } - return super.dataTypeCode( typeName ); + if ( typeName.equalsIgnoreCase( "timestamp" ) ) { + return Types.TIMESTAMP; + } + if ( typeName.equalsIgnoreCase( "timestamp with time zone" ) + || typeName.equalsIgnoreCase( "timestamp with local time zone" ) ) { + return Types.TIMESTAMP_WITH_TIMEZONE; + } + if ( typeName.equalsIgnoreCase( "clob" ) ) { + return Types.CLOB; + } + if ( typeName.equalsIgnoreCase( "blob" ) ) { + return Types.BLOB; + } + if ( typeName.equalsIgnoreCase( "raw" ) ) { + return Types.VARBINARY; + } + if ( typeName.equalsIgnoreCase( "long raw" ) ) { + return Types.LONGVARBINARY; + } + if ( typeName.equalsIgnoreCase( "ref cursor" ) ) { + return Types.REF_CURSOR; + } + if ( typeName.equalsIgnoreCase( "number" ) ) { + return Types.NUMERIC; + } + if ( typeName.equalsIgnoreCase( "date" ) ) { + return Types.DATE; + } + if ( typeName.equalsIgnoreCase( "nvarchar2" ) ) { + return Types.NVARCHAR; + } + if ( typeName.equalsIgnoreCase( "varchar2" ) ) { + return Types.VARCHAR; + } + return 0; } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/spi/ReactiveNamedNativeQueryMemento.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/spi/ReactiveNamedNativeQueryMemento.java index fc1493d02..9d639d099 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/spi/ReactiveNamedNativeQueryMemento.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/spi/ReactiveNamedNativeQueryMemento.java @@ -52,6 +52,11 @@ public Class getResultMappingClass() { return delegate.getResultMappingClass(); } + @Override + public Class getResultType() { + return delegate.getResultType(); + } + @Override public Integer getFirstResult() { return delegate.getFirstResult(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveAbstractCteMutationHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveAbstractCteMutationHandler.java index d1ea9026f..a75872f43 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveAbstractCteMutationHandler.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveAbstractCteMutationHandler.java @@ -30,8 +30,8 @@ import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; import org.hibernate.query.sqm.tree.expression.SqmParameter; +import org.hibernate.reactive.engine.spi.ReactiveSharedSessionContractImplementor; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveAbstractMutationHandler; -import org.hibernate.reactive.session.ReactiveSession; import org.hibernate.reactive.sql.exec.internal.StandardReactiveSelectExecutor; import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; import org.hibernate.sql.ast.SqlAstTranslator; @@ -179,7 +179,7 @@ public MappingModelExpressible getResolvedMappingModelType(SqmParameter StandardReactiveSelectExecutor.INSTANCE.list( select, diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertHandler.java index aaf1be3e9..86f0a8bea 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertHandler.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertHandler.java @@ -43,10 +43,10 @@ import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; import org.hibernate.query.sqm.tree.insert.SqmValues; +import org.hibernate.reactive.engine.spi.ReactiveSharedSessionContractImplementor; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.query.sqm.mutation.internal.ReactiveHandler; -import org.hibernate.reactive.session.ReactiveSession; import org.hibernate.reactive.sql.exec.internal.StandardReactiveSelectExecutor; import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; import org.hibernate.spi.NavigablePath; @@ -575,7 +575,7 @@ public MappingModelExpressible getResolvedMappingModelType(SqmParameter StandardReactiveSelectExecutor.INSTANCE.list( select, diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveGlobalTemporaryTableStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveGlobalTemporaryTableStrategy.java index 37c3d23bc..d92062e2f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveGlobalTemporaryTableStrategy.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveGlobalTemporaryTableStrategy.java @@ -65,22 +65,23 @@ default void prepare(MappingModelCreationProcess mappingModelCreationProcess, Jd if ( !createIdTables ) { tableCreatedStage.complete( null ); } - - LOG.debugf( "Creating global-temp ID table : %s", getTemporaryTable().getTableExpression() ); - - connectionStage() - .thenCompose( this::createTable ) - .whenComplete( (connection, throwable) -> releaseConnection( connection ) - .thenAccept( v -> { - if ( throwable == null ) { - setDropIdTables( configService ); - tableCreatedStage.complete( null ); - } - else { - tableCreatedStage.completeExceptionally( throwable ); - } - } ) - ); + else { + LOG.debugf( "Creating global-temp ID table : %s", getTemporaryTable().getTableExpression() ); + + connectionStage() + .thenCompose( this::createTable ) + .whenComplete( (connection, throwable) -> releaseConnection( connection ) + .thenAccept( v -> { + if ( throwable == null ) { + setDropIdTables( configService ); + tableCreatedStage.complete( null ); + } + else { + tableCreatedStage.completeExceptionally( throwable ); + } + } ) + ); + } } private CompletionStage releaseConnection(ReactiveConnection connection) { @@ -147,24 +148,25 @@ default void release( if ( !isDropIdTables() ) { tableDroppedStage.complete( null ); } - - setDropIdTables( false ); - - final TemporaryTable temporaryTable = getTemporaryTable(); - LOG.debugf( "Dropping global-tempk ID table : %s", temporaryTable.getTableExpression() ); - - connectionStage() - .thenCompose( this::dropTable ) - .whenComplete( (connection, throwable) -> releaseConnection( connection ) - .thenAccept( v -> { - if ( throwable == null ) { - tableDroppedStage.complete( null ); - } - else { - tableDroppedStage.completeExceptionally( throwable ); - } - } ) - ); + else { + setDropIdTables( false ); + + final TemporaryTable temporaryTable = getTemporaryTable(); + LOG.debugf( "Dropping global-temp ID table : %s", temporaryTable.getTableExpression() ); + + connectionStage() + .thenCompose( this::dropTable ) + .whenComplete( (connection, throwable) -> releaseConnection( connection ) + .thenAccept( v -> { + if ( throwable == null ) { + tableDroppedStage.complete( null ); + } + else { + tableDroppedStage.completeExceptionally( throwable ); + } + } ) + ); + } } private CompletionStage dropTable(ReactiveConnection connection) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactivePersistentTableStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactivePersistentTableStrategy.java index 472dc0ce7..ba70601ad 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactivePersistentTableStrategy.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactivePersistentTableStrategy.java @@ -68,22 +68,23 @@ default void prepare(MappingModelCreationProcess mappingModelCreationProcess, Jd if ( !createIdTables ) { tableCreatedStage.complete( null ); } - - LOG.debugf( "Creating persistent ID table : %s", getTemporaryTable().getTableExpression() ); - - connectionStage() - .thenCompose( this::createTable ) - .whenComplete( (connection, throwable) -> releaseConnection( connection ) - .thenAccept( v -> { - if ( throwable == null ) { - setDropIdTables( configService ); - tableCreatedStage.complete( null ); - } - else { - tableCreatedStage.completeExceptionally( throwable ); - } - } ) - ); + else { + LOG.debugf( "Creating persistent ID table : %s", getTemporaryTable().getTableExpression() ); + + connectionStage() + .thenCompose( this::createTable ) + .whenComplete( (connection, throwable) -> releaseConnection( connection ) + .thenAccept( v -> { + if ( throwable == null ) { + setDropIdTables( configService ); + tableCreatedStage.complete( null ); + } + else { + tableCreatedStage.completeExceptionally( throwable ); + } + } ) + ); + } } private CompletionStage releaseConnection(ReactiveConnection connection) { @@ -150,24 +151,25 @@ default void release( if ( !isDropIdTables() ) { tableDroppedStage.complete( null ); } - - setDropIdTables( false ); - - final TemporaryTable temporaryTable = getTemporaryTable(); - LOG.debugf( "Dropping persistent ID table : %s", temporaryTable.getTableExpression() ); - - connectionStage() - .thenCompose( this::dropTable ) - .whenComplete( (connection, throwable) -> releaseConnection( connection ) - .thenAccept( v -> { - if ( throwable == null ) { - tableDroppedStage.complete( null ); - } - else { - tableDroppedStage.completeExceptionally( throwable ); - } - } ) - ); + else { + setDropIdTables( false ); + + final TemporaryTable temporaryTable = getTemporaryTable(); + LOG.debugf( "Dropping persistent ID table : %s", temporaryTable.getTableExpression() ); + + connectionStage() + .thenCompose( this::dropTable ) + .whenComplete( (connection, throwable) -> releaseConnection( connection ) + .thenAccept( v -> { + if ( throwable == null ) { + tableDroppedStage.complete( null ); + } + else { + tableDroppedStage.completeExceptionally( throwable ); + } + } ) + ); + } } private CompletionStage dropTable(ReactiveConnection connection) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTemporaryTableHelper.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTemporaryTableHelper.java index d027235a2..4eaecd04f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTemporaryTableHelper.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTemporaryTableHelper.java @@ -68,11 +68,8 @@ public TemporaryTableCreationWork( @Override public CompletionStage reactiveExecute(ReactiveConnection connection) { - final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); - try { final String creationCommand = exporter.getSqlCreateCommand( temporaryTable ); - logStatement( creationCommand, jdbcServices ); return connection.executeUnprepared( creationCommand ) .handle( (integer, throwable) -> { @@ -116,11 +113,8 @@ public TemporaryTableDropWork( @Override public CompletionStage reactiveExecute(ReactiveConnection connection) { - final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); - try { final String dropCommand = exporter.getSqlDropCommand( temporaryTable ); - logStatement( dropCommand, jdbcServices ); return connection.update( dropCommand ) .handle( (integer, throwable) -> { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java index f3a24808d..a46185772 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java @@ -135,6 +135,7 @@ import static org.hibernate.reactive.persister.entity.impl.ReactiveEntityPersister.forceInitialize; import static org.hibernate.reactive.session.impl.SessionUtil.checkEntityFound; import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; +import static org.hibernate.reactive.util.impl.CompletionStages.failedFuture; import static org.hibernate.reactive.util.impl.CompletionStages.nullFuture; import static org.hibernate.reactive.util.impl.CompletionStages.rethrow; import static org.hibernate.reactive.util.impl.CompletionStages.returnNullorRethrow; @@ -963,7 +964,7 @@ public CompletionStage reactiveForceFlush(EntityEntry entry) { } if ( getPersistenceContextInternal().getCascadeLevel() > 0 ) { - return CompletionStages.failedFuture( new ObjectDeletedException( + return failedFuture( new ObjectDeletedException( "deleted object would be re-saved by cascade (remove deleted object from associations)", entry.getId(), entry.getPersister().getEntityName() @@ -1616,7 +1617,23 @@ public void close() throws HibernateException { @Override public CompletionStage reactiveClose() { - super.close(); + try { + super.close(); + return closeConnection(); + } + catch (RuntimeException e) { + return closeConnection() + .handle( CompletionStages::handle ) + .thenCompose( closeConnectionHandler -> { + if ( closeConnectionHandler.hasFailed() ) { + LOG.errorClosingConnection( closeConnectionHandler.getThrowable() ); + } + return failedFuture( e ); + } ); + } + } + + private CompletionStage closeConnection() { return reactiveConnection != null ? reactiveConnection.close() : voidFuture(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableAssembler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableAssembler.java new file mode 100644 index 000000000..db8222838 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableAssembler.java @@ -0,0 +1,42 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.sql.results.graph.embeddable.internal; + +import org.hibernate.reactive.sql.exec.spi.ReactiveRowProcessingState; +import org.hibernate.reactive.sql.results.graph.ReactiveDomainResultsAssembler; +import org.hibernate.reactive.sql.results.graph.ReactiveInitializer; +import org.hibernate.sql.results.graph.Initializer; +import org.hibernate.sql.results.graph.InitializerData; +import org.hibernate.sql.results.graph.embeddable.EmbeddableInitializer; +import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableAssembler; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; + +import java.util.concurrent.CompletionStage; + +import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; + +/** + * @see org.hibernate.sql.results.graph.embeddable.internal.EmbeddableAssembler + */ +public class ReactiveEmbeddableAssembler extends EmbeddableAssembler implements ReactiveDomainResultsAssembler { + + public ReactiveEmbeddableAssembler(EmbeddableInitializer initializer) { + super( initializer ); + } + + @Override + public CompletionStage reactiveAssemble(ReactiveRowProcessingState rowProcessingState, JdbcValuesSourceProcessingOptions options) { + final ReactiveInitializer reactiveInitializer = (ReactiveInitializer) getInitializer(); + final InitializerData data = reactiveInitializer.getData( rowProcessingState ); + final Initializer.State state = data.getState(); + if ( state == Initializer.State.KEY_RESOLVED ) { + return reactiveInitializer + .reactiveResolveInstance( data ) + .thenApply( v -> reactiveInitializer.getResolvedInstance( data ) ); + } + return completedFuture( reactiveInitializer.getResolvedInstance( data ) ); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableFetchImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableFetchImpl.java index 50034533b..8a5ec18f9 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableFetchImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableFetchImpl.java @@ -6,14 +6,20 @@ package org.hibernate.reactive.sql.results.graph.embeddable.internal; import org.hibernate.engine.FetchTiming; +import org.hibernate.reactive.sql.results.graph.entity.internal.ReactiveEntityFetchSelectImpl; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.results.graph.AssemblerCreationState; +import org.hibernate.sql.results.graph.DomainResultAssembler; import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.Fetch; import org.hibernate.sql.results.graph.FetchParent; +import org.hibernate.sql.results.graph.Fetchable; +import org.hibernate.sql.results.graph.Initializer; import org.hibernate.sql.results.graph.InitializerParent; import org.hibernate.sql.results.graph.embeddable.EmbeddableInitializer; import org.hibernate.sql.results.graph.embeddable.EmbeddableValuedFetchable; import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableFetchImpl; +import org.hibernate.sql.results.graph.entity.internal.EntityFetchSelectImpl; public class ReactiveEmbeddableFetchImpl extends EmbeddableFetchImpl { @@ -37,4 +43,19 @@ public EmbeddableInitializer createInitializer( AssemblerCreationState creationState) { return new ReactiveEmbeddableInitializerImpl( this, getDiscriminatorFetch(), parent, creationState, true ); } + + @Override + public DomainResultAssembler createAssembler(InitializerParent parent, AssemblerCreationState creationState) { + Initializer initializer = creationState.resolveInitializer( this, parent, this ); + EmbeddableInitializer embeddableInitializer = initializer.asEmbeddableInitializer(); + return new ReactiveEmbeddableAssembler( embeddableInitializer ); + } + + @Override + public Fetch findFetch(Fetchable fetchable) { + Fetch fetch = super.findFetch( fetchable ); + return fetch instanceof EntityFetchSelectImpl + ? new ReactiveEntityFetchSelectImpl( (EntityFetchSelectImpl) fetch ) + : fetch; + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableForeignKeyResultImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableForeignKeyResultImpl.java index a708481cf..1e3433320 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableForeignKeyResultImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableForeignKeyResultImpl.java @@ -10,7 +10,6 @@ import org.hibernate.sql.results.graph.InitializerParent; import org.hibernate.sql.results.graph.embeddable.EmbeddableInitializer; import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableForeignKeyResultImpl; -import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableInitializerImpl; public class ReactiveEmbeddableForeignKeyResultImpl extends EmbeddableForeignKeyResultImpl { @@ -22,6 +21,6 @@ public ReactiveEmbeddableForeignKeyResultImpl(EmbeddableForeignKeyResultImpl public EmbeddableInitializer createInitializer(InitializerParent parent, AssemblerCreationState creationState) { return getReferencedModePart() instanceof NonAggregatedIdentifierMapping ? new ReactiveNonAggregatedIdentifierMappingInitializer( this, null, creationState, true ) - : new EmbeddableInitializerImpl( this, null, null, creationState, true ); + : new ReactiveEmbeddableInitializerImpl( this, null, null, creationState, true ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableInitializerImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableInitializerImpl.java index 69ae34a73..d5f9db5e2 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableInitializerImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableInitializerImpl.java @@ -5,12 +5,19 @@ */ package org.hibernate.reactive.sql.results.graph.embeddable.internal; + import java.util.concurrent.CompletionStage; import java.util.function.BiFunction; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.mapping.VirtualModelPart; +import org.hibernate.metamodel.spi.EmbeddableInstantiator; +import org.hibernate.reactive.sql.exec.spi.ReactiveRowProcessingState; +import org.hibernate.reactive.sql.results.graph.ReactiveDomainResultsAssembler; import org.hibernate.reactive.sql.results.graph.ReactiveInitializer; import org.hibernate.sql.results.graph.AssemblerCreationState; +import org.hibernate.sql.results.graph.DomainResultAssembler; import org.hibernate.sql.results.graph.Initializer; import org.hibernate.sql.results.graph.InitializerData; import org.hibernate.sql.results.graph.InitializerParent; @@ -19,12 +26,19 @@ import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableInitializerImpl; import org.hibernate.sql.results.jdbc.spi.RowProcessingState; +import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; import static org.hibernate.reactive.util.impl.CompletionStages.loop; +import static org.hibernate.reactive.util.impl.CompletionStages.nullFuture; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; +import static org.hibernate.reactive.util.impl.CompletionStages.whileLoop; +import static org.hibernate.sql.results.graph.embeddable.EmbeddableLoadingLogger.EMBEDDED_LOAD_LOGGER; +import static org.hibernate.sql.results.graph.entity.internal.BatchEntityInsideEmbeddableSelectFetchInitializer.BATCH_PROPERTY; public class ReactiveEmbeddableInitializerImpl extends EmbeddableInitializerImpl implements ReactiveInitializer { + private final SessionFactoryImplementor sessionFactory; + private static class ReactiveEmbeddableInitializerData extends EmbeddableInitializerData { public ReactiveEmbeddableInitializerData( @@ -33,6 +47,20 @@ public ReactiveEmbeddableInitializerData( super( initializer, rowProcessingState ); } + public Object[] getRowState(){ + return rowState; + } + + @Override + public void setState(State state) { + super.setState( state ); + if ( State.UNINITIALIZED == state ) { + // reset instance to null as otherwise EmbeddableInitializerImpl#prepareCompositeInstance + // will never create a new instance after the "first row with a non-null instance" gets processed + setInstance( null ); + } + } + public EmbeddableMappingType.ConcreteEmbeddableType getConcreteEmbeddableType() { return super.concreteEmbeddableType; } @@ -45,6 +73,7 @@ public ReactiveEmbeddableInitializerImpl( AssemblerCreationState creationState, boolean isResultInitializer) { super( resultDescriptor, discriminatorFetch, parent, creationState, isResultInitializer ); + sessionFactory = creationState.getSqlAstCreationContext().getSessionFactory(); } @Override @@ -54,10 +83,129 @@ protected InitializerData createInitializerData(RowProcessingState rowProcessing @Override public CompletionStage reactiveResolveInstance(EmbeddableInitializerData data) { - super.resolveInstance( data ); + if ( data.getState() != State.KEY_RESOLVED ) { + return voidFuture(); + } + + data.setState( State.RESOLVED ); + return extractRowState( (ReactiveEmbeddableInitializerData) data ) + .thenCompose( unused -> prepareCompositeInstance( (ReactiveEmbeddableInitializerData) data ) ); + } + + private CompletionStage extractRowState(ReactiveEmbeddableInitializerData data) { + final DomainResultAssembler[] subAssemblers = assemblers[data.getSubclassId()]; + final RowProcessingState rowProcessingState = data.getRowProcessingState(); + final Object[] rowState = data.getRowState(); + final boolean[] stateAllNull = {true}; + final int[] index = {0}; + final boolean[] forceExit = { false }; + return whileLoop( + () -> index[0] < subAssemblers.length && !forceExit[0], + () -> { + final int i = index[0]++; + final DomainResultAssembler assembler = subAssemblers[i]; + if ( assembler instanceof ReactiveDomainResultsAssembler ) { + return ( (ReactiveDomainResultsAssembler) assembler ) + .reactiveAssemble( (ReactiveRowProcessingState) rowProcessingState ) + .thenAccept( contributorValue -> setContributorValue( + contributorValue, + i, + rowState, + stateAllNull, + forceExit + ) ); + } + else { + setContributorValue( + assembler == null ? null : assembler.assemble( rowProcessingState ), + i, + rowState, + stateAllNull, + forceExit + ); + return voidFuture(); + } + }) + .whenComplete( + (unused, throwable) -> { + if ( stateAllNull[0] ) { + data.setState( State.MISSING ); + } + } + ); + } + + private void setContributorValue( + Object contributorValue, + int index, + Object[] rowState, + boolean[] stateAllNull, + boolean[] forceExit) { + if ( contributorValue == BATCH_PROPERTY ) { + rowState[index] = null; + } + else { + rowState[index] = contributorValue; + } + if ( contributorValue != null ) { + stateAllNull[0] = false; + } + else if ( isPartOfKey() ) { + // If this is a foreign key and there is a null part, the whole thing has to be turned into null + stateAllNull[0] = true; + forceExit[0] = true; + } + } + + private CompletionStage prepareCompositeInstance(ReactiveEmbeddableInitializerData data) { + // Virtual model parts use the owning entity as container which the fetch parent access provides. + // For an identifier or foreign key this is called during the resolveKey phase of the fetch parent, + // so we can't use the fetch parent access in that case. + final ReactiveInitializer parent = (ReactiveInitializer) getParent(); + if ( parent != null && getInitializedPart() instanceof VirtualModelPart && !isPartOfKey() && data.getState() != State.MISSING ) { + final ReactiveEmbeddableInitializerData subData = parent.getData( data.getRowProcessingState() ); + return parent + .reactiveResolveInstance( subData ) + .thenCompose( + unused -> { + data.setInstance( parent.getResolvedInstance( subData ) ); + if ( data.getState() == State.INITIALIZED ) { + return voidFuture(); + } + return doCreateCompositeInstance( data ) + .thenAccept( v -> EMBEDDED_LOAD_LOGGER.debugf( + "Created composite instance [%s]", + getNavigablePath() + ) ); + } ); + } + + return doCreateCompositeInstance( data ) + .thenAccept( v -> EMBEDDED_LOAD_LOGGER.debugf( "Created composite instance [%s]", getNavigablePath() ) ); + + } + + private CompletionStage doCreateCompositeInstance(ReactiveEmbeddableInitializerData data) { + if ( data.getInstance() == null ) { + return createCompositeInstance( data ) + .thenAccept( data::setInstance ); + } return voidFuture(); } + private CompletionStage createCompositeInstance(ReactiveEmbeddableInitializerData data) { + if ( data.getState() == State.MISSING ) { + return nullFuture(); + } + + final EmbeddableInstantiator instantiator = data.getConcreteEmbeddableType() == null + ? getInitializedPart().getEmbeddableTypeDescriptor().getRepresentationStrategy().getInstantiator() + : data.getConcreteEmbeddableType().getInstantiator(); + final Object instance = instantiator.instantiate( data, sessionFactory ); + data.setState( State.RESOLVED ); + return completedFuture( instance ); + } + @Override public CompletionStage reactiveInitializeInstance(EmbeddableInitializerData data) { super.initializeInstance( data ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveNonAggregatedIdentifierMappingInitializer.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveNonAggregatedIdentifierMappingInitializer.java index 0096e8d0f..51f7d59b7 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveNonAggregatedIdentifierMappingInitializer.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveNonAggregatedIdentifierMappingInitializer.java @@ -60,9 +60,8 @@ public CompletionStage reactiveResolveKey(NonAggregatedIdentifierMappingIn } else { final RowProcessingState rowProcessingState = data.getRowProcessingState(); - final boolean[] dataIsMissing = {false}; return loop( getInitializers(), initializer -> { - if ( dataIsMissing[0] ) { + if ( initializer == null ) { return voidFuture(); } final InitializerData subData = ( (ReactiveInitializer) initializer ) @@ -72,7 +71,6 @@ public CompletionStage reactiveResolveKey(NonAggregatedIdentifierMappingIn .thenAccept( v -> { if ( subData.getState() == State.MISSING ) { data.setState( State.MISSING ); - dataIsMissing[0] = true; } } ); } ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityInitializerImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityInitializerImpl.java index a00287eab..70d8335ac 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityInitializerImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityInitializerImpl.java @@ -25,8 +25,9 @@ import org.hibernate.persister.entity.EntityPersister; import org.hibernate.proxy.LazyInitializer; import org.hibernate.proxy.map.MapProxy; -import org.hibernate.reactive.session.ReactiveSession; +import org.hibernate.reactive.session.ReactiveQueryProducer; import org.hibernate.reactive.sql.exec.spi.ReactiveRowProcessingState; +import org.hibernate.reactive.sql.results.graph.ReactiveDomainResultsAssembler; import org.hibernate.reactive.sql.results.graph.ReactiveInitializer; import org.hibernate.sql.results.graph.AssemblerCreationState; import org.hibernate.sql.results.graph.DomainResult; @@ -254,16 +255,21 @@ public CompletionStage reactiveResolveInstance(EntityInitializerData origi final RowProcessingState rowProcessingState = data.getRowProcessingState(); data.setState( State.RESOLVED ); if ( data.getEntityKey() == null ) { - assert getIdentifierAssembler() != null; - final Object id = getIdentifierAssembler().assemble( rowProcessingState ); - if ( id == null ) { - setMissing( data ); - return voidFuture(); - } - resolveEntityKey( data, id ); + return assembleId( rowProcessingState ) + .thenCompose( id -> { + if ( id == null ) { + setMissing( data ); + return voidFuture(); + } + resolveEntityKey( data, id ); + return postAssembleId( rowProcessingState, data ); + } ); } - final PersistenceContext persistenceContext = rowProcessingState.getSession() - .getPersistenceContextInternal(); + return postAssembleId( rowProcessingState, data ); + } + + private CompletionStage postAssembleId(RowProcessingState rowProcessingState, ReactiveEntityInitializerData data) { + final PersistenceContext persistenceContext = rowProcessingState.getSession().getPersistenceContextInternal(); data.setEntityHolder( persistenceContext.claimEntityHolderIfPossible( data.getEntityKey(), null, @@ -274,29 +280,39 @@ public CompletionStage reactiveResolveInstance(EntityInitializerData origi if ( useEmbeddedIdentifierInstanceAsEntity( data ) ) { data.setEntityInstanceForNotify( rowProcessingState.getEntityId() ); data.setInstance( data.getEntityInstanceForNotify() ); + postResolveInstance( data ); + return voidFuture(); } - else { - return reactiveResolveEntityInstance1( data ) - .thenAccept( v -> { - if ( data.getUniqueKeyAttributePath() != null ) { - final SharedSessionContractImplementor session = rowProcessingState.getSession(); - final EntityPersister concreteDescriptor = getConcreteDescriptor( data ); - final EntityUniqueKey euk = new EntityUniqueKey( - concreteDescriptor.getEntityName(), - data.getUniqueKeyAttributePath(), - rowProcessingState.getEntityUniqueKey(), - data.getUniqueKeyPropertyTypes()[concreteDescriptor.getSubclassId()], - session.getFactory() - ); - session.getPersistenceContextInternal().addEntity( euk, data.getInstance() ); - } - postResolveInstance( data ); - } ); + + return reactiveResolveEntityInstance1( data ) + .thenAccept( v -> { + if ( data.getUniqueKeyAttributePath() != null ) { + final SharedSessionContractImplementor session = rowProcessingState.getSession(); + final EntityPersister concreteDescriptor = getConcreteDescriptor( data ); + final EntityUniqueKey euk = new EntityUniqueKey( + concreteDescriptor.getEntityName(), + data.getUniqueKeyAttributePath(), + rowProcessingState.getEntityUniqueKey(), + data.getUniqueKeyPropertyTypes()[concreteDescriptor.getSubclassId()], + session.getFactory() + ); + session.getPersistenceContextInternal().addEntity( euk, data.getInstance() ); + } + postResolveInstance( data ); + } ); + } + + private CompletionStage assembleId(RowProcessingState rowProcessingState) { + final DomainResultAssembler identifierAssembler = getIdentifierAssembler(); + assert identifierAssembler != null; + if ( identifierAssembler instanceof ReactiveDomainResultsAssembler ) { + return ( (ReactiveDomainResultsAssembler) identifierAssembler ) + .reactiveAssemble( (ReactiveRowProcessingState) rowProcessingState ); } - postResolveInstance( data ); - return voidFuture(); + return completedFuture( identifierAssembler.assemble( rowProcessingState ) ); } + // We could move this method in ORM private void postResolveInstance(ReactiveEntityInitializerData data) { if ( data.getInstance() != null ) { upgradeLockMode( data ); @@ -532,7 +548,7 @@ protected CompletionStage reactiveResolveEntityInstance(ReactiveEntityIn // If this initializer owns the entity, we have to remove the entity holder, // because the subsequent loading process will claim the entity session.getPersistenceContextInternal().removeEntityHolder( data.getEntityKey() ); - return ( (ReactiveSession) session ).reactiveInternalLoad( + return ( (ReactiveQueryProducer) session ).reactiveInternalLoad( data.getConcreteDescriptor().getEntityName(), data.getEntityKey().getIdentifier(), true, diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntitySelectFetchInitializer.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntitySelectFetchInitializer.java index b27c0cc93..6d5ea5418 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntitySelectFetchInitializer.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntitySelectFetchInitializer.java @@ -23,7 +23,7 @@ import org.hibernate.proxy.LazyInitializer; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; -import org.hibernate.reactive.session.ReactiveSession; +import org.hibernate.reactive.session.ReactiveQueryProducer; import org.hibernate.reactive.sql.results.graph.ReactiveInitializer; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.results.graph.AssemblerCreationState; @@ -56,6 +56,16 @@ public Object getEntityIdentifier() { public void setEntityIdentifier(Object entityIdentifier) { super.entityIdentifier = entityIdentifier; } + + @Override + public void setState(State state) { + super.setState( state ); + if ( State.UNINITIALIZED == state ) { + // reset instance to null as otherwise EmbeddableInitializerImpl#prepareCompositeInstance + // will never create a new instance after the "first row with a non-null instance" gets processed + setInstance( null ); + } + } } private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); @@ -144,7 +154,7 @@ else if ( data.getInstance() == null ) { data.setState( State.INITIALIZED ); final String entityName = concreteDescriptor.getEntityName(); - return ( (ReactiveSession) session ).reactiveInternalLoad( + return ( (ReactiveQueryProducer) session ).reactiveInternalLoad( entityName, data.getEntityIdentifier(), true, diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDeferredResultSetAccess.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDeferredResultSetAccess.java index da94cc329..3b185152a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDeferredResultSetAccess.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDeferredResultSetAccess.java @@ -149,9 +149,10 @@ private CompletionStage executeQuery() { return completedFuture( logicalConnection ) .thenCompose( lg -> { LOG.tracef( "Executing query to retrieve ResultSet : %s", getFinalSql() ); - Dialect dialect = DialectDelegateWrapper.extractRealDialect( executionContext.getSession().getJdbcServices().getDialect() ); - // I'm not sure calling Parameters here is necessary, the query should already have the right parameters + + // This must happen at the very last minute in order to process parameters + // added in org.hibernate.dialect.pagination.OffsetFetchLimitHandler.processSql final String sql = Parameters.instance( dialect ).process( getFinalSql() ); Object[] parameters = PreparedStatementAdaptor.bind( super::bindParameters ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java index 47da9f6cf..3a6164a39 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java @@ -2314,6 +2314,31 @@ default CompletionStage withStatelessTransaction(Function implements SelectionQuery { - private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); private final ReactiveSelectionQuery delegate; public StageSelectionQueryImpl(ReactiveSelectionQuery delegate) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionFactoryImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionFactoryImpl.java index b03df1fc9..2c359192d 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionFactoryImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionFactoryImpl.java @@ -138,6 +138,16 @@ private CompletionStage connection(String tenantId) { : connectionPool.getConnection( tenantId ); } + @Override + public Stage.Session getCurrentSession() { + return context.get( contextKeyForSession ); + } + + @Override + public Stage.StatelessSession getCurrentStatelessSession() { + return context.get( contextKeyForStatelessSession ); + } + @Override public CompletionStage withSession(Function> work) { Objects.requireNonNull( work, "parameter 'work' is required" ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BaseReactiveTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BaseReactiveTest.java index 6cd705f2b..1a5f51e3b 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BaseReactiveTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BaseReactiveTest.java @@ -18,6 +18,8 @@ import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableStrategy; +import org.hibernate.query.sqm.mutation.internal.temptable.PersistentTableStrategy; import org.hibernate.reactive.containers.DatabaseConfiguration; import org.hibernate.reactive.containers.DatabaseConfiguration.DBType; import org.hibernate.reactive.mutiny.Mutiny; @@ -105,6 +107,8 @@ public static void setDefaultProperties(Configuration configuration) { configuration.setProperty( Settings.SHOW_SQL, System.getProperty( Settings.SHOW_SQL, "false" ) ); configuration.setProperty( Settings.FORMAT_SQL, System.getProperty( Settings.FORMAT_SQL, "false" ) ); configuration.setProperty( Settings.HIGHLIGHT_SQL, System.getProperty( Settings.HIGHLIGHT_SQL, "true" ) ); + configuration.setProperty( PersistentTableStrategy.DROP_ID_TABLES, "true" ); + configuration.setProperty( GlobalTemporaryTableStrategy.DROP_ID_TABLES, "true" ); } public static final SessionFactoryManager factoryManager = new SessionFactoryManager(); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdTest.java index de3590fab..14ac0d56f 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdTest.java @@ -42,6 +42,19 @@ public void populateDb(VertxTestContext context) { test( context, getMutinySessionFactory().withTransaction( s -> s.persistAll( pizza, schnitzel ) ) ); } + @Test + public void testStatelessInsert(VertxTestContext context) { + LocationId nottingham = new LocationId( "UK", "Nottingham" ); + Delivery mushyPeas = new Delivery( nottingham, "Mushy Peas with mint sauce" ); + test( context, getMutinySessionFactory() + .withStatelessTransaction( s -> s.insert( mushyPeas ) ) + .chain( () -> getMutinySessionFactory() + .withTransaction( s -> s.find( Delivery.class, nottingham ) ) + ) + .invoke( result -> assertThat( result ).isEqualTo( mushyPeas ) ) + ); + } + @Test public void testFindSingleId(VertxTestContext context) { test( context, getMutinySessionFactory().withTransaction( s -> s.find( Delivery.class, verbania ) ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdWithManyEagerTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdWithManyEagerTest.java new file mode 100644 index 000000000..0ceeaef18 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdWithManyEagerTest.java @@ -0,0 +1,228 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +public class EmbeddedIdWithManyEagerTest extends BaseReactiveTest { + + Fruit cherry; + Fruit apple; + Fruit banana; + + Flower sunflower; + Flower chrysanthemum; + Flower rose; + + @Override + protected Collection> annotatedEntities() { + return List.of( Flower.class, Fruit.class ); + } + + @BeforeEach + public void populateDb(VertxTestContext context) { + Seed seed1 = new Seed( 1 ); + rose = new Flower( seed1, "Rose" ); + + cherry = new Fruit( seed1, "Cherry" ); + cherry.addFriend( rose ); + + Seed seed2 = new Seed( 2 ); + sunflower = new Flower( seed2, "Sunflower" ); + + apple = new Fruit( seed2, "Apple" ); + apple.addFriend( sunflower ); + + Seed seed3 = new Seed( 3 ); + chrysanthemum = new Flower( seed3, "Chrysanthemum" ); + + banana = new Fruit( seed3, "Banana" ); + banana.addFriend( chrysanthemum ); + + test( + context, + getMutinySessionFactory().withTransaction( s -> s + .persistAll( cherry, rose, sunflower, apple, chrysanthemum, banana ) + ) + ); + } + + @Test + public void testFindWithEmbeddedId(VertxTestContext context) { + test( + context, getMutinySessionFactory().withTransaction( s -> s + .find( Flower.class, chrysanthemum.getSeed() ) + .invoke( flower -> assertThat( flower.getName() ).isEqualTo( chrysanthemum.getName() ) ) + ) + ); + } + + @Test + public void testSelectQueryWithEmbeddedId(VertxTestContext context) { + test( + context, getMutinySessionFactory().withTransaction( s -> s + .createSelectionQuery( "from Flower", Flower.class ) + .getResultList() + .invoke( list -> assertThat( list.stream().map( Flower::getName ) ) + .containsExactlyInAnyOrder( + sunflower.getName(), + chrysanthemum.getName(), + rose.getName() + ) + ) + ) + ); + } + + @Test + public void testSelectQueryWithEmbeddedIdAndEagerRelation(VertxTestContext context) { + test( + context, getMutinySessionFactory().withTransaction( s -> s + .createSelectionQuery( "from Flower f ", Flower.class ) + .getResultList() + .invoke( list -> assertThat( list.stream().map( Flower::getFriend ).map( Plant::getName ) ) + .containsExactlyInAnyOrder( + cherry.getName(), + apple.getName(), + banana.getName() + ) + ) + ) + ); + } + + @Embeddable + public static class Seed { + + @Column(nullable = false, updatable = false) + private Integer id; + + public Seed() { + } + + public Seed(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + } + + @MappedSuperclass + public static abstract class Plant { + + @EmbeddedId + private Seed seed; + + @Column(length = 40, unique = true) + private String name; + + protected Plant() { + } + + protected Plant(Seed seed, String name) { + this.seed = seed; + this.name = name; + } + + public Seed getSeed() { + return seed; + } + + public void setSeed(Seed seed) { + this.seed = seed; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity(name = "Fruit") + @Table(name = "known_fruits") + public static class Fruit extends Plant { + + @OneToMany(mappedBy = "friend", fetch = FetchType.LAZY) + private List friends = new ArrayList<>(); + + public Fruit() { + } + + public Fruit(Seed seed, String name) { + super( seed, name ); + } + + public void addFriend(Flower flower) { + this.friends.add( flower ); + flower.friend = this; + } + + public List getFriends() { + return friends; + } + + @Override + public String toString() { + return "Fruit{" + getSeed().getId() + "," + getName() + '}'; + } + + } + + @Entity(name = "Flower") + @Table(name = "known_flowers") + public static class Flower extends Plant { + + @ManyToOne(fetch = FetchType.EAGER, optional = false) + @JoinColumn(name = "friend", referencedColumnName = "id", nullable = false) + private Fruit friend; + + public Flower() { + } + + public Flower(Seed seed, String name) { + super( seed, name ); + } + + public Fruit getFriend() { + return friend; + } + + @Override + public String toString() { + return "Flower{" + getSeed().getId() + "," + getName() + '}'; + } + + } + +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdWithManyTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdWithManyTest.java new file mode 100644 index 000000000..eed1e5846 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdWithManyTest.java @@ -0,0 +1,207 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EmbeddedIdWithManyTest extends BaseReactiveTest { + + Fruit cherry; + Fruit apple; + Fruit banana; + + Flower sunflower; + Flower chrysanthemum; + Flower rose; + + @Override + protected Collection> annotatedEntities() { + return List.of( Flower.class, Fruit.class ); + } + + @BeforeEach + public void populateDb(VertxTestContext context) { + Seed seed1 = new Seed( 1 ); + rose = new Flower( seed1, "Rose" ); + + cherry = new Fruit( seed1, "Cherry" ); + cherry.addFriend( rose ); + + Seed seed2 = new Seed( 2 ); + sunflower = new Flower( seed2, "Sunflower" ); + + apple = new Fruit( seed2, "Apple" ); + apple.addFriend( sunflower ); + + Seed seed3 = new Seed( 3 ); + chrysanthemum = new Flower( seed3, "Chrysanthemum" ); + + banana = new Fruit( seed3, "Banana" ); + banana.addFriend( chrysanthemum ); + + test( + context, + getMutinySessionFactory().withTransaction( s -> s + .persistAll( cherry, rose, sunflower, apple, chrysanthemum, banana ) + ) + ); + } + + @Test + public void testFindWithEmbeddedId(VertxTestContext context) { + test( + context, getMutinySessionFactory().withTransaction( s -> s + .find( Flower.class, chrysanthemum.getSeed() ) + .invoke( flower -> assertThat( flower.getName() ).isEqualTo( chrysanthemum.getName() ) ) + ) + ); + } + + @Test + public void testSelectQueryWithEmbeddedId(VertxTestContext context) { + test( + context, getMutinySessionFactory().withTransaction( s -> s + .createSelectionQuery( "from Flower", Flower.class ) + .getResultList() + .invoke( list -> assertThat( list.stream().map( Flower::getName ) ) + .containsExactlyInAnyOrder( + sunflower.getName(), + chrysanthemum.getName(), + rose.getName() + ) + ) + ) + ); + } + + @Embeddable + public static class Seed { + + @Column(nullable = false, updatable = false) + private Integer id; + + public Seed() { + } + + public Seed(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + } + + @MappedSuperclass + public static abstract class Plant { + + @EmbeddedId + private Seed seed; + + @Column(length = 40, unique = true) + private String name; + + protected Plant() { + } + + protected Plant(Seed seed, String name) { + this.seed = seed; + this.name = name; + } + + public Seed getSeed() { + return seed; + } + + public void setSeed(Seed seed) { + this.seed = seed; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity(name = "Fruit") + @Table(name = "known_fruits") + public static class Fruit extends Plant { + + @OneToMany(mappedBy = "friend", fetch = FetchType.LAZY) + private List friends = new ArrayList<>(); + + public Fruit() { + } + + public Fruit(Seed seed, String name) { + super( seed, name ); + } + + public void addFriend(Flower flower) { + this.friends.add( flower ); + flower.friend = this; + } + + public List getFriends() { + return friends; + } + + @Override + public String toString() { + return "Fruit{" + getSeed().getId() + "," + getName() + '}'; + } + + } + + @Entity(name = "Flower") + @Table(name = "known_flowers") + public static class Flower extends Plant { + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "friend", referencedColumnName = "id", nullable = false) + private Fruit friend; + + public Flower() { + } + + public Flower(Seed seed, String name) { + super( seed, name ); + } + + @Override + public String toString() { + return "Flower{" + getSeed().getId() + "," + getName() + '}'; + } + + } + +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdWithOneToOneTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdWithOneToOneTest.java new file mode 100644 index 000000000..f5cbf5fd9 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdWithOneToOneTest.java @@ -0,0 +1,131 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EmbeddedIdWithOneToOneTest extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return List.of( FooEntity.class, BarEntity.class ); + } + + @Test + public void test(VertxTestContext context) { + BarEntity barEntity = new BarEntity( "1" ); + FooId fooId = new FooId( barEntity ); + FooEntity entity = new FooEntity( fooId ); + + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.persistAll( barEntity, entity ) ) + .chain( () -> getMutinySessionFactory() + .withTransaction( s -> s.find( FooEntity.class, fooId ) ) + ) + .invoke( result -> { + assertThat( result.getId() ).isEqualTo( fooId ); + assertThat( result.getId().getIdEntity() ).isEqualTo( fooId.getIdEntity() ); + assertThat( result.getId().getIdEntity().getId() ).isEqualTo( fooId.getIdEntity().getId() ); + } ) + ); + } + + @Entity(name = "bar") + public static class BarEntity { + + @Id + private String id; + + public BarEntity() { + } + + public BarEntity(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + } + + @Entity(name = "foo") + public static class FooEntity { + + @EmbeddedId + private FooId id; + + public FooEntity() { + } + + public FooEntity(FooId id) { + this.id = id; + } + + public FooId getId() { + return id; + } + + public void setId(FooId id) { + this.id = id; + } + } + + @Embeddable + public static class FooId { + + @OneToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "id", nullable = false) + private BarEntity barEntity; + + public FooId() { + } + + public FooId(BarEntity barEntity) { + this.barEntity = barEntity; + } + + public BarEntity getIdEntity() { + return barEntity; + } + + public void setIdEntity(BarEntity barEntity) { + this.barEntity = barEntity; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + FooId fooId = (FooId) o; + return Objects.equals( barEntity, fooId.barEntity ); + } + + @Override + public int hashCode() { + return Objects.hashCode( barEntity ); + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/HQLQueryTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/HQLQueryTest.java index e48d3ec69..77331fe2b 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/HQLQueryTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/HQLQueryTest.java @@ -5,6 +5,7 @@ */ package org.hibernate.reactive; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; @@ -17,32 +18,41 @@ import io.vertx.junit5.Timeout; import io.vertx.junit5.VertxTestContext; import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import static jakarta.persistence.CascadeType.PERSIST; +import static jakarta.persistence.FetchType.LAZY; import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; @Timeout(value = 10, timeUnit = MINUTES) - public class HQLQueryTest extends BaseReactiveTest { Flour spelt = new Flour( 1, "Spelt", "An ancient grain, is a hexaploid species of wheat.", "Wheat flour" ); Flour rye = new Flour( 2, "Rye", "Used to bake the traditional sourdough breads of Germany.", "Wheat flour" ); Flour almond = new Flour( 3, "Almond", "made from ground almonds.", "Gluten free" ); + Author miller = new Author( "Madeline Miller"); + Author camilleri = new Author( "Andrea Camilleri"); + Book circe = new Book( "9780316556347", "Circe", miller ); + Book shapeOfWater = new Book( "0-330-49286-1 ", "The Shape of Water", camilleri ); + Book spider = new Book( "978-0-14-311203-7", "The Patience of the Spider", camilleri ); + @Override protected Collection> annotatedEntities() { - return List.of( Flour.class ); + return List.of( Flour.class, Book.class, Author.class ); } @BeforeEach public void populateDb(VertxTestContext context) { - test( context, getMutinySessionFactory() - .withTransaction( (session, transaction) -> session.persistAll( spelt, rye, almond ) ) ); + test( context, getMutinySessionFactory().withTransaction( session -> session + .persistAll( spelt, rye, almond, miller, camilleri, circe, shapeOfWater, spider ) ) + ); } @Test @@ -69,7 +79,7 @@ public void testAutoFlushOnResultList(VertxTestContext context) { public void testSelectScalarString(VertxTestContext context) { test( context, getSessionFactory().withSession( s -> { Stage.SelectionQuery qr = s.createSelectionQuery( "SELECT 'Prova' FROM Flour WHERE id = " + rye.getId(), String.class ); - assertNotNull( qr ); + assertThat( qr ).isNotNull(); return qr.getSingleResult(); } ).thenAccept( found -> assertEquals( "Prova", found ) ) ); } @@ -78,7 +88,7 @@ public void testSelectScalarString(VertxTestContext context) { public void testSelectScalarCount(VertxTestContext context) { test( context, getSessionFactory().withSession( s -> { Stage.SelectionQuery qr = s.createSelectionQuery( "SELECT count(*) FROM Flour", Long.class ); - assertNotNull( qr ); + assertThat( qr ).isNotNull(); return qr.getSingleResult(); } ).thenAccept( found -> assertEquals( 3L, found ) ) ); } @@ -86,14 +96,17 @@ public void testSelectScalarCount(VertxTestContext context) { @Test public void testSelectWithMultipleScalarValues(VertxTestContext context) { test( context, getSessionFactory().withSession( s -> { - Stage.SelectionQuery qr = s.createSelectionQuery( "SELECT 'Prova', f.id FROM Flour f WHERE f.id = " + rye.getId(), Object[].class ); - assertNotNull( qr ); - return qr.getSingleResult(); - } ).thenAccept( found -> { - assertTrue( found instanceof Object[] ); - assertEquals( "Prova", ( (Object[]) found )[0] ); - assertEquals( rye.getId(), ( (Object[]) found )[1] ); - } ) + Stage.SelectionQuery qr = s.createSelectionQuery( + "SELECT 'Prova', f.id FROM Flour f WHERE f.id = " + rye.getId(), + Object[].class + ); + assertThat( qr ).isNotNull(); + return qr.getSingleResult(); + } ).thenAccept( found -> { + assertThat( found ).isInstanceOf( Object[].class ); + assertEquals( "Prova", ( (Object[]) found )[0] ); + assertEquals( rye.getId(), ( (Object[]) found )[1] ); + } ) ); } @@ -101,7 +114,7 @@ public void testSelectWithMultipleScalarValues(VertxTestContext context) { public void testSingleResultQueryOnId(VertxTestContext context) { test( context, getSessionFactory().withSession( s -> { Stage.SelectionQuery qr = s.createSelectionQuery( "FROM Flour WHERE id = 1", Flour.class ); - assertNotNull( qr ); + assertThat( qr ).isNotNull(); return qr.getSingleResult(); } ).thenAccept( flour -> assertEquals( spelt, flour ) ) ); @@ -111,7 +124,7 @@ public void testSingleResultQueryOnId(VertxTestContext context) { public void testSingleResultQueryOnName(VertxTestContext context) { test( context, getSessionFactory().withSession( s -> { Stage.SelectionQuery qr = s.createSelectionQuery( "FROM Flour WHERE name = 'Almond'", Flour.class ); - assertNotNull( qr ); + assertThat( qr ).isNotNull(); return qr.getSingleResult(); } ).thenAccept( flour -> assertEquals( almond, flour ) ) ); @@ -122,13 +135,28 @@ public void testFromQuery(VertxTestContext context) { test( context, getSessionFactory() .withSession( s -> { Stage.SelectionQuery qr = s.createSelectionQuery( "FROM Flour ORDER BY name", Flour.class ); - assertNotNull( qr ); + assertThat( qr ).isNotNull(); return qr.getResultList(); } ) .thenAccept( results -> assertThat( results ).containsExactly( almond, rye, spelt ) ) ); } + @Test + public void testSelectNewConstructor(VertxTestContext context) { + test( context, getMutinySessionFactory() + .withTransaction( session -> session + .createQuery( "SELECT NEW Book(b.title, b.author) FROM Book b ORDER BY b.title DESC", Book.class ) + .getResultList() + ) + .invoke( books -> assertThat( books ).containsExactly( + new Book( shapeOfWater.title, camilleri ), + new Book( spider.title, camilleri ), + new Book( circe.title, miller ) + ) ) + ); + } + @Entity(name = "Flour") @Table(name = "Flour") public static class Flour { @@ -204,4 +232,122 @@ public int hashCode() { return Objects.hash( name, description, type ); } } + + @Entity(name = "Book") + @Table(name = "Book_HQL") + public static class Book { + @Id + @GeneratedValue + private Integer id; + + private String isbn; + + private String title; + + @ManyToOne(fetch = LAZY) + private Author author; + + public Book() { + } + + public Book(String title, Author author) { + this.title = title; + this.author = author; + } + + public Book(String isbn, String title, Author author) { + this.isbn = isbn; + this.title = title; + this.author = author; + author.books.add( this ); + } + + public Integer getId() { + return id; + } + + public String getIsbn() { + return isbn; + } + + public String getTitle() { + return title; + } + + public Author getAuthor() { + return author; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Book book = (Book) o; + return Objects.equals( isbn, book.isbn ) && Objects.equals( + title, + book.title + ) && Objects.equals( author, book.author ); + } + + @Override + public int hashCode() { + return Objects.hash( isbn, title, author ); + } + + @Override + public String toString() { + return id + ":" + isbn + ":" + title + ":" + author; + } + } + + @Entity(name = "Author") + @Table(name = "Author_HQL") + public static class Author { + @Id @GeneratedValue + private Integer id; + + private String name; + + @OneToMany(mappedBy = "author", cascade = PERSIST) + private List books = new ArrayList<>(); + + public Author() { + } + + public Author(String name) { + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public List getBooks() { + return books; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Author author = (Author) o; + return Objects.equals( name, author.name ); + } + + @Override + public int hashCode() { + return Objects.hashCode( name ); + } + + @Override + public String toString() { + return id + ":" + name; + } + } } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ManyToOneIdClassTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ManyToOneIdClassTest.java new file mode 100644 index 000000000..73dead49a --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ManyToOneIdClassTest.java @@ -0,0 +1,135 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.Collection; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.ManyToOne; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; + +@Timeout(value = 1, timeUnit = MINUTES) +public class ManyToOneIdClassTest extends BaseReactiveTest { + + private final static String USER_NAME = "user"; + private final static String SUBSYSTEM_ID = "1"; + + @Override + protected Collection> annotatedEntities() { + return List.of( SystemUser.class, Subsystem.class ); + } + + @BeforeEach + public void populateDb(VertxTestContext context) { + Subsystem subsystem = new Subsystem( SUBSYSTEM_ID, "sub 1" ); + SystemUser systemUser = new SystemUser( subsystem, USER_NAME, "system 1" ); + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.persistAll( subsystem, systemUser ) ) + ); + } + + @Test + public void testQuery(VertxTestContext context) { + test( + context, openSession().thenAccept( session -> session + .createQuery( "FROM SystemUser s", SystemUser.class ) + .getResultList().thenAccept( list -> { + assertThat( list ).hasSize( 1 ); + SystemUser systemUser = list.get( 0 ); + assertThat( systemUser.getSubsystem().getId() ).isEqualTo( SUBSYSTEM_ID ); + assertThat( systemUser.getUsername() ).isEqualTo( USER_NAME ); + } ) ) + ); + } + + @Entity(name = "SystemUser") + @IdClass(PK.class) + public static class SystemUser { + + @Id + @ManyToOne(fetch = FetchType.LAZY) + private Subsystem subsystem; + + @Id + private String username; + + private String name; + + public SystemUser() { + } + + public SystemUser(Subsystem subsystem, String username, String name) { + this.subsystem = subsystem; + this.username = username; + this.name = name; + } + + public Subsystem getSubsystem() { + return subsystem; + } + + public String getUsername() { + return username; + } + + public String getName() { + return name; + } + } + + @Entity(name = "Subsystem") + public static class Subsystem { + + @Id + private String id; + + private String description; + + public Subsystem() { + } + + public Subsystem(String id, String description) { + this.id = id; + this.description = description; + } + + public String getId() { + return id; + } + + public String getDescription() { + return description; + } + } + + public static class PK { + + private Subsystem subsystem; + + private String username; + + public PK(Subsystem subsystem, String username) { + this.subsystem = subsystem; + this.username = username; + } + + private PK() { + } + } + +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinyStatelessSessionTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinyStatelessSessionTest.java new file mode 100644 index 000000000..5d325b5f6 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinyStatelessSessionTest.java @@ -0,0 +1,250 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.*; +import jakarta.persistence.criteria.*; +import org.junit.jupiter.api.Test; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.junit.jupiter.api.Assertions.*; + +@Timeout(value = 10, timeUnit = MINUTES) + +public class MutinyStatelessSessionTest extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return List.of( GuineaPig.class ); + } + + @Test + public void testStatelessSession(VertxTestContext context) { + GuineaPig pig = new GuineaPig( "Aloi" ); + test( context, getMutinySessionFactory().withStatelessSession( ss -> ss + .insert( pig ) + .chain( v -> ss.createSelectionQuery( "from GuineaPig where name=:n", GuineaPig.class ) + .setParameter( "n", pig.name ) + .getResultList() ) + .invoke( list -> { + assertFalse( list.isEmpty() ); + assertEquals( 1, list.size() ); + assertThatPigsAreEqual( pig, list.get( 0 ) ); + } ) + .chain( v -> ss.get( GuineaPig.class, pig.id ) ) + .chain( p -> { + assertThatPigsAreEqual( pig, p ); + p.name = "X"; + return ss.update( p ); + } ) + .chain( v -> ss.refresh( pig ) ) + .invoke( v -> assertEquals( pig.name, "X" ) ) + .chain( v -> ss.createMutationQuery( "update GuineaPig set name='Y'" ).executeUpdate() ) + .chain( v -> ss.refresh( pig ) ) + .invoke( v -> assertEquals( pig.name, "Y" ) ) + .chain( v -> ss.delete( pig ) ) + .chain( v -> ss.createSelectionQuery( "from GuineaPig", GuineaPig.class ).getResultList() ) + .invoke( list -> assertTrue( list.isEmpty() ) ) ) + ); + } + + @Test + public void testStatelessSessionWithNamed(VertxTestContext context) { + GuineaPig pig = new GuineaPig( "Aloi" ); + test( context, getMutinySessionFactory().withStatelessSession( ss -> ss + .insert( pig ) + .chain( v -> ss.createNamedQuery( "findbyname", GuineaPig.class ) + .setParameter( "n", pig.name ) + .getResultList() ) + .invoke( list -> { + assertFalse( list.isEmpty() ); + assertEquals( 1, list.size() ); + assertThatPigsAreEqual( pig, list.get( 0 ) ); + } ) + .chain( v -> ss.get( GuineaPig.class, pig.id ) ) + .chain( p -> { + assertThatPigsAreEqual( pig, p ); + p.name = "X"; + return ss.update( p ); + } ) + .chain( v -> ss.refresh( pig ) ) + .invoke( v -> assertEquals( pig.name, "X" ) ) + .chain( v -> ss.createNamedQuery( "updatebyname" ).executeUpdate() ) + .chain( v -> ss.refresh( pig ) ) + .invoke( v -> assertEquals( pig.name, "Y" ) ) + .chain( v -> ss.delete( pig ) ) + .chain( v -> ss.createNamedQuery( "findall" ).getResultList() ) + .invoke( list -> assertTrue( list.isEmpty() ) ) ) + ); + } + + @Test + public void testStatelessSessionWithNative(VertxTestContext context) { + GuineaPig pig = new GuineaPig( "Aloi" ); + test( context, getMutinySessionFactory().openStatelessSession() + .chain( ss -> ss.insert( pig ) + .chain( v -> ss + .createNativeQuery( "select * from Piggy where name=:n", GuineaPig.class ) + .setParameter( "n", pig.name ) + .getResultList() ) + .invoke( list -> { + assertFalse( list.isEmpty() ); + assertEquals( 1, list.size() ); + assertThatPigsAreEqual( pig, list.get( 0 ) ); + } ) + .chain( v -> ss.get( GuineaPig.class, pig.id ) ) + .chain( p -> { + assertThatPigsAreEqual( pig, p ); + p.name = "X"; + return ss.update( p ); + } ) + .chain( v -> ss.refresh( pig ) ) + .invoke( v -> assertEquals( pig.name, "X" ) ) + .chain( v -> ss.createNativeQuery( "update Piggy set name='Y'" ) + .executeUpdate() ) + .invoke( rows -> assertEquals( 1, rows ) ) + .chain( v -> ss.refresh( pig ) ) + .invoke( v -> assertEquals( pig.name, "Y" ) ) + .chain( v -> ss.delete( pig ) ) + .chain( v -> ss.createNativeQuery( "select id from Piggy" ).getResultList() ) + .invoke( list -> assertTrue( list.isEmpty() ) ) + .chain( v -> ss.close() ) ) + ); + } + + @Test + public void testStatelessSessionCriteria(VertxTestContext context) { + GuineaPig pig = new GuineaPig( "Aloi" ); + GuineaPig mate = new GuineaPig("Aloina"); + pig.mate = mate; + + CriteriaBuilder cb = getSessionFactory().getCriteriaBuilder(); + + CriteriaQuery query = cb.createQuery( GuineaPig.class ); + Root gp = query.from( GuineaPig.class ); + query.where( cb.equal( gp.get( "name" ), cb.parameter( String.class, "n" ) ) ); + query.orderBy( cb.asc( gp.get( "name" ) ) ); + + test( context, getMutinySessionFactory().openStatelessSession() + .chain( ss -> ss.insert(mate) + .chain( v -> ss.insert(pig) ) + .chain( v -> ss.createQuery( query ) + .setParameter( "n", pig.name ) + .getResultList() ) + .invoke( list -> { + assertFalse( list.isEmpty() ); + assertEquals( 1, list.size() ); + assertThatPigsAreEqual( pig, list.get( 0 ) ); + } ) + .chain( v -> ss.close() ) ) + ); + } + + @Test + public void testTransactionPropagation(VertxTestContext context) { + test( context, getMutinySessionFactory().withStatelessSession( + session -> session.withTransaction( transaction -> session.createSelectionQuery( "from GuineaPig", GuineaPig.class ) + .getResultList() + .chain( list -> { + assertNotNull( session.currentTransaction() ); + assertFalse( session.currentTransaction().isMarkedForRollback() ); + session.currentTransaction().markForRollback(); + assertTrue( session.currentTransaction().isMarkedForRollback() ); + assertTrue( transaction.isMarkedForRollback() ); + return session.withTransaction( t -> { + assertTrue( t.isMarkedForRollback() ); + return session.createSelectionQuery( "from GuineaPig", GuineaPig.class ).getResultList(); + } ); + } ) ) + ) ); + } + + @Test + public void testSessionPropagation(VertxTestContext context) { + test( context, getMutinySessionFactory().withStatelessSession( + session -> session.createSelectionQuery( "from GuineaPig", GuineaPig.class ).getResultList() + .chain( list -> getMutinySessionFactory().withStatelessSession( s -> { + assertEquals( session, s ); + return s.createSelectionQuery( "from GuineaPig", GuineaPig.class ).getResultList(); + } ) ) + ) ); + } + + private void assertThatPigsAreEqual( GuineaPig expected, GuineaPig actual) { + assertNotNull( actual ); + assertEquals( expected.getId(), actual.getId() ); + assertEquals( expected.getName(), actual.getName() ); + } + + @NamedQuery(name = "findbyname", query = "from GuineaPig where name=:n") + @NamedQuery(name = "updatebyname", query = "update GuineaPig set name='Y'") + @NamedQuery(name = "findall", query = "from GuineaPig") + + @Entity(name = "GuineaPig") + @Table(name = "Piggy") + public static class GuineaPig { + @Id + @GeneratedValue + private Integer id; + private String name; + @Version + private int version; + + @ManyToOne + private GuineaPig mate; + + public GuineaPig() { + } + + public GuineaPig(String name) { + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return id + ": " + name; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + GuineaPig guineaPig = (GuineaPig) o; + return Objects.equals( name, guineaPig.name ); + } + + @Override + public int hashCode() { + return Objects.hash( name ); + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyArrayMergeTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyArrayMergeTest.java new file mode 100644 index 000000000..e2eb826d9 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyArrayMergeTest.java @@ -0,0 +1,190 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; + +@Timeout(value = 2, timeUnit = MINUTES) +public class OneToManyArrayMergeTest extends BaseReactiveTest { + + private final static Long USER_ID = 1L; + private final static Long ADMIN_ROLE_ID = 2L; + private final static Long USER_ROLE_ID = 3L; + private final static String UPDATED_FIRSTNAME = "UPDATED FIRSTNAME"; + private final static String UPDATED_LASTNAME = "UPDATED LASTNAME"; + + @Override + protected Collection> annotatedEntities() { + return List.of( User.class, Role.class ); + } + + @BeforeEach + public void populateDb(VertxTestContext context) { + Role adminRole = new Role( ADMIN_ROLE_ID, "admin" ); + Role userRole = new Role( USER_ROLE_ID, "user" ); + User user = new User( USER_ID, "first", "last", adminRole ); + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.persistAll( user, adminRole, userRole ) ) + ); + } + + @Test + public void testMerge(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + .chain( user -> getMutinySessionFactory() + .withTransaction( s -> s + .createQuery( "FROM Role", Role.class ) + .getResultList() ) + .map( roles -> { + user.addAll( roles ); + user.setFirstname( UPDATED_FIRSTNAME ); + user.setLastname( UPDATED_LASTNAME ); + return user; + } ) + ) + .chain( user -> { + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).hasSize( 2 ); + return getMutinySessionFactory() + .withTransaction( s -> s.merge( user ) ); + } + ) + .chain( v -> getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + ) + .invoke( user -> { + Role adminRole = new Role( ADMIN_ROLE_ID, "admin" ); + Role userRole = new Role( USER_ROLE_ID, "user" ); + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).containsExactlyInAnyOrder( + adminRole, + userRole + ); + } + ) + ); + } + + @Entity(name = "User") + @Table(name = "USER_TABLE") + public static class User { + + @Id + private Long id; + + private String firstname; + + private String lastname; + + @OneToMany(fetch = FetchType.EAGER) + private Role[] roles; + + public User() { + } + + public User(Long id, String firstname, String lastname, Role... roles) { + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + this.roles = new Role[roles.length]; + for ( int i = 0; i < roles.length; i++ ) { + this.roles[i] = roles[i]; + } + } + + public Long getId() { + return id; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public Role[] getRoles() { + return roles; + } + + public void addAll(List roles) { + this.roles = new Role[roles.size()]; + for ( int i = 0; i < roles.size(); i++ ) { + this.roles[i] = roles.get( i ); + } + } + } + + @Entity(name = "Role") + @Table(name = "ROLE_TABLE") + public static class Role { + + @Id + private Long id; + private String code; + + public Role() { + } + + public Role(Long id, String code) { + this.id = id; + this.code = code; + } + + public Object getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Role role = (Role) o; + return Objects.equals( id, role.id ) && Objects.equals( code, role.code ); + } + + @Override + public int hashCode() { + return Objects.hash( id, code ); + } + + @Override + public String toString() { + return "Role{" + code + '}'; + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMapMergeTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMapMergeTest.java new file mode 100644 index 000000000..7a1096d3d --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMapMergeTest.java @@ -0,0 +1,199 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; + +@Timeout(value = 2, timeUnit = MINUTES) +public class OneToManyMapMergeTest extends BaseReactiveTest { + + private final static Long USER_ID = 1L; + private final static Long ADMIN_ROLE_ID = 2L; + private final static Long USER_ROLE_ID = 3L; + private final static String UPDATED_FIRSTNAME = "UPDATED FIRSTNAME"; + private final static String UPDATED_LASTNAME = "UPDATED LASTNAME"; + + @Override + protected Collection> annotatedEntities() { + return List.of( User.class, Role.class ); + } + + @BeforeEach + public void populateDb(VertxTestContext context) { + Role adminRole = new Role( ADMIN_ROLE_ID, "admin" ); + Role userRole = new Role( USER_ROLE_ID, "user" ); + User user = new User( USER_ID, "first", "last", adminRole ); + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.persistAll( user, adminRole, userRole ) ) + ); + } + + @Test + public void testMerge(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + .chain( user -> getMutinySessionFactory() + .withTransaction( s -> s + .createQuery( "FROM Role", Role.class ) + .getResultList() ) + .map( roles -> { + user.addAll( roles ); + user.setFirstname( UPDATED_FIRSTNAME ); + user.setLastname( UPDATED_LASTNAME ); + return user; + } ) + ) + .chain( user -> { + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).hasSize( 2 ); + return getMutinySessionFactory() + .withTransaction( s -> s.merge( user ) ); + } + ) + .chain( v -> getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + ) + .invoke( user -> { + Role adminRole = new Role( ADMIN_ROLE_ID, "admin" ); + Role userRole = new Role( USER_ROLE_ID, "user" ); + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).containsEntry( + adminRole.getCode(), + adminRole + ); + assertThat( user.getRoles() ).containsEntry( + userRole.getCode(), + userRole + ); + } + ) + ); + } + + @Entity(name = "User") + @Table(name = "USER_TABLE") + public static class User { + + @Id + private Long id; + + private String firstname; + + private String lastname; + + @OneToMany(fetch = FetchType.EAGER) + private Map roles = new HashMap(); + + public User() { + } + + public User(Long id, String firstname, String lastname, Role... roles) { + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + for ( Role role : roles ) { + this.roles.put( role.getCode(), role ); + } + } + + public Long getId() { + return id; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public Map getRoles() { + return roles; + } + + public void addAll(List roles) { + this.roles.clear(); + for ( Role role : roles ) { + this.roles.put( role.getCode(), role ); + } + } + } + + @Entity(name = "Role") + @Table(name = "ROLE_TABLE") + public static class Role { + + @Id + private Long id; + private String code; + + public Role() { + } + + public Role(Long id, String code) { + this.id = id; + this.code = code; + } + + public Object getId() { + return id; + } + + public String getCode() { + return code; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Role role = (Role) o; + return Objects.equals( id, role.id ) && Objects.equals( code, role.code ); + } + + @Override + public int hashCode() { + return Objects.hash( id, code ); + } + + @Override + public String toString() { + return "Role{" + code + '}'; + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMergeTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMergeTest.java new file mode 100644 index 000000000..76d7f7732 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyMergeTest.java @@ -0,0 +1,227 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; + +@Timeout(value = 2, timeUnit = MINUTES) +public class OneToManyMergeTest extends BaseReactiveTest { + + private final static Long USER_ID = 1L; + private final static Long ADMIN_ROLE_ID = 2L; + private final static Long USER_ROLE_ID = 3L; + private final static String UPDATED_FIRSTNAME = "UPDATED FIRSTNAME"; + private final static String UPDATED_LASTNAME = "UPDATED LASTNAME"; + + @Override + protected Collection> annotatedEntities() { + return List.of( User.class, Role.class ); + } + + @BeforeEach + public void populateDb(VertxTestContext context) { + Role adminRole = new Role( ADMIN_ROLE_ID, "admin" ); + Role userRole = new Role( USER_ROLE_ID, "user" ); + User user = new User( USER_ID, "first", "last", adminRole ); + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.persistAll( user, adminRole, userRole ) ) + ); + } + + @Test + public void testMerge(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + .chain( user -> getMutinySessionFactory() + .withTransaction( s -> s + .createQuery( "FROM Role", Role.class ) + .getResultList() ) + .map( roles -> { + user.getRoles().clear(); + user.getRoles().addAll( roles ); + user.setFirstname( UPDATED_FIRSTNAME ); + user.setLastname( UPDATED_LASTNAME ); + return user; + } ) + ) + .chain( user -> { + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).hasSize( 2 ); + return getMutinySessionFactory() + .withTransaction( s -> s.merge( user ) ); + } + ) + .chain( v -> getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + ) + .invoke( user -> { + Role adminRole = new Role( ADMIN_ROLE_ID, "admin" ); + Role userRole = new Role( USER_ROLE_ID, "user" ); + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).containsExactlyInAnyOrder( + adminRole, + userRole + ); + } + ) + ); + } + + @Test + public void testMergeRemovingCollectionElements(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + .chain( user -> getMutinySessionFactory() + .withTransaction( s -> s + .createQuery( "FROM Role", Role.class ) + .getResultList() ) + .map( roles -> { + user.clearRoles(); + user.setFirstname( UPDATED_FIRSTNAME ); + user.setLastname( UPDATED_LASTNAME ); + return user; + } ) + ) + .chain( user -> { + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).isNull(); + return getMutinySessionFactory() + .withTransaction( s -> s.merge( user ) ); + } + ) + .chain( v -> getMutinySessionFactory() + .withTransaction( s -> s.find( User.class, USER_ID ) ) + ) + .invoke( user -> { + assertThat( user.getFirstname() ).isEqualTo( UPDATED_FIRSTNAME ); + assertThat( user.getLastname() ).isEqualTo( UPDATED_LASTNAME ); + assertThat( user.getRoles() ).isNullOrEmpty(); + } + ) + ); + } + + @Entity(name = "User") + @Table(name = "USER_TABLE") + public static class User { + + @Id + private Long id; + + private String firstname; + + private String lastname; + + @OneToMany(fetch = FetchType.EAGER) + private List roles; + + public User() { + } + + public User(Long id, String firstname, String lastname, Role... roles) { + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + this.roles = List.of( roles ); + } + + public User(Long id, String firstname, String lastname) { + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + } + + public Long getId() { + return id; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public List getRoles() { + return roles; + } + + public void clearRoles() { + this.roles = null; + } + } + + @Entity(name = "Role") + @Table(name = "ROLE_TABLE") + public static class Role { + + @Id + private Long id; + private String code; + + public Role() { + } + + public Role(Long id, String code) { + this.id = id; + this.code = code; + } + + public Object getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Role role = (Role) o; + return Objects.equals( id, role.id ) && Objects.equals( code, role.code ); + } + + @Override + public int hashCode() { + return Objects.hash( id, code ); + } + + @Override + public String toString() { + return "Role{" + code + '}'; + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToOneMapsIdAndGeneratedIdTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToOneMapsIdAndGeneratedIdTest.java new file mode 100644 index 000000000..7aca2c611 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToOneMapsIdAndGeneratedIdTest.java @@ -0,0 +1,154 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; + +@Timeout(value = 10, timeUnit = MINUTES) +public class OneToOneMapsIdAndGeneratedIdTest extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return List.of( Person.class, NaturalPerson.class ); + } + + @Test + public void testPersist(VertxTestContext context) { + NaturalPerson naturalPerson = new NaturalPerson( "natual" ); + Person person = new Person( "person", naturalPerson ); + + test( + context, getMutinySessionFactory() + .withTransaction( session -> session.persist( person ) ) + .chain( () -> getMutinySessionFactory() + .withTransaction( session -> session + .find( Person.class, person.getId() ) + .invoke( result -> { + assertThat( result ).isNotNull(); + assertThat( result.getNaturalPerson() ).isNotNull(); + assertThat( result.getNaturalPerson().getId() ).isEqualTo( result.getId() ); + } ) ) + ) + ); + + } + + @Entity + public static class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + @OneToOne(mappedBy = "person", cascade = CascadeType.ALL) + private NaturalPerson naturalPerson; + + public Person() { + } + + public Person(String name, NaturalPerson naturalPerson) { + this.name = name; + this.naturalPerson = naturalPerson; + naturalPerson.person = this; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public NaturalPerson getNaturalPerson() { + return naturalPerson; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Person person = (Person) o; + return Objects.equals( name, person.name ); + } + + @Override + public int hashCode() { + return Objects.hashCode( name ); + } + } + + @Entity + public static class NaturalPerson { + + @Id + private Long id; + + @Column + private String name; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Person person; + + public NaturalPerson() { + } + + public NaturalPerson(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Person getPerson() { + return person; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + NaturalPerson that = (NaturalPerson) o; + return Objects.equals( name, that.name ); + } + + @Override + public int hashCode() { + return Objects.hashCode( name ); + } + } + + +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/QueryTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/QueryTest.java index d6146b2e5..44c822718 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/QueryTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/QueryTest.java @@ -43,6 +43,7 @@ import static jakarta.persistence.FetchType.LAZY; import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; import static org.hibernate.reactive.QueryTest.Author.AUTHOR_TABLE; import static org.hibernate.reactive.QueryTest.Author.HQL_NAMED_QUERY; import static org.hibernate.reactive.QueryTest.Author.SQL_NAMED_QUERY; @@ -395,6 +396,42 @@ public void testNativeEntityQueryWithParam(VertxTestContext context) { ); } + // https://github.com/hibernate/hibernate-reactive/issues/2314 + @Test + public void testNativeEntityQueryWithLimit(VertxTestContext context) { + Author author1 = new Author( "Iain M. Banks" ); + Author author2 = new Author( "Neal Stephenson" ); + Book book1 = new Book( "1-85723-235-6", "Feersum Endjinn", author1 ); + Book book2 = new Book( "0-380-97346-4", "Cryptonomicon", author2 ); + Book book3 = new Book( "0-553-08853-X", "Snow Crash", author2 ); + author1.books.add( book1 ); + author2.books.add( book2 ); + author2.books.add( book3 ); + + test( + context, + openSession() + .thenCompose( session -> session.persist( author1, author2 ) + .thenCompose( v -> session.flush() ) + ) + .thenCompose( v -> openSession() ) + .thenCompose( session -> session.createNativeQuery( + "select * from " + BOOK_TABLE + " order by isbn", + Book.class + ) + .setMaxResults( 2 ) + .getResultList() ) + .thenAccept( books -> { + assertThat( books ) + .extracting( b -> b.id, b -> b.title, b -> b.isbn ) + .containsExactly( + tuple( book2.id, book2.title, book2.isbn ), + tuple( book3.id, book3.title, book3.isbn ) + ); + } ) + ); + } + @Test public void testNativeEntityQueryWithNamedParam(VertxTestContext context) { Author author1 = new Author( "Iain M. Banks" ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/TimestampTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/TimestampTest.java index 8a6f631fc..7841216c7 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/TimestampTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/TimestampTest.java @@ -6,6 +6,7 @@ package org.hibernate.reactive; import java.time.Instant; +import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.List; @@ -18,6 +19,9 @@ import io.vertx.junit5.Timeout; import io.vertx.junit5.VertxTestContext; import jakarta.persistence.Basic; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; @@ -28,12 +32,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @Timeout(value = 10, timeUnit = MINUTES) - public class TimestampTest extends BaseReactiveTest { @Override protected Collection> annotatedEntities() { - return List.of( Record.class ); + return List.of( Record.class, Event.class ); } @Test @@ -56,6 +59,30 @@ public void test(VertxTestContext context) { ); } + @Test + public void testEmbedded(VertxTestContext context) { + Event event = new Event(); + History history = new History(); + event.name = "Concert"; + test( context, getMutinySessionFactory() + .withSession( session -> session.persist( event ) + .chain( session::flush ) + .invoke( () -> { + history.created = event.history.created; + history.updated = event.history.updated; + assertEquals( + event.history.created.truncatedTo( ChronoUnit.HOURS ), + event.history.updated.truncatedTo( ChronoUnit.HOURS ) + ); }) + .invoke( () -> event.name = "Conference" ) + .chain( session::flush ) + .invoke( () -> assertInstants( event, history ) ) ) + .chain( () -> getMutinySessionFactory().withSession( session -> session + .find( Record.class, event.id ) ) ) + .invoke( r -> assertInstants( event, history ) ) + ); + } + private static void assertInstants(Record r) { assertNotNull( r.created ); assertNotNull( r.updated ); @@ -66,6 +93,18 @@ private static void assertInstants(Record r) { ); } + private static void assertInstants(Event e, History h) { + assertNotNull( e.history.created ); + assertNotNull( e.history.updated ); + // Sometimes, when the test suite is fast enough, they might be the same + assertTrue( + !e.history.updated.isBefore( e.history.created ), + "Updated instant is before created. Updated[" + e.history.updated + "], Created[" + e.history.created + "]" + ); + assertEquals( h.created, e.history.created ); + + } + @Entity(name = "Record") static class Record { @GeneratedValue @@ -78,4 +117,30 @@ static class Record { @UpdateTimestamp Instant updated; } + + @Entity(name = "Event") + static class Event { + + @Id + @GeneratedValue + public Long id; + + public String name; + + @Embedded + public History history; + + } + + @Embeddable + static class History { + @Column + @CreationTimestamp + public LocalDateTime created; + + @Column + @UpdateTimestamp + public LocalDateTime updated; + + } } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/CockroachDBDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/CockroachDBDatabase.java index 103723127..d8bedf9ac 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/CockroachDBDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/CockroachDBDatabase.java @@ -12,7 +12,7 @@ import org.testcontainers.containers.CockroachContainer; import org.testcontainers.containers.Container; -import static org.hibernate.reactive.containers.DockerImage.imageName; +import static org.hibernate.reactive.containers.DockerImage.fromDockerfile; class CockroachDBDatabase extends PostgreSQLDatabase { @@ -25,7 +25,7 @@ class CockroachDBDatabase extends PostgreSQLDatabase { * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located * at `$HOME/.testcontainers.properties` (create the file if it does not exist). */ - public static final CockroachContainer cockroachDb = new CockroachContainer( imageName( "cockroachdb/cockroach", "v24.1.0" ) ) + public static final CockroachContainer cockroachDb = new CockroachContainer( fromDockerfile( "cockroachdb" ) ) // Username, password and database are not supported by test container at the moment // Testcontainers will use a database named 'postgres' and the 'root' user .withReuse( true ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java index b7ac63e4d..29154b2bf 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java @@ -28,7 +28,7 @@ import org.testcontainers.containers.Db2Container; -import static org.hibernate.reactive.containers.DockerImage.imageName; +import static org.hibernate.reactive.containers.DockerImage.fromDockerfile; class DB2Database implements TestableDatabase { @@ -87,7 +87,7 @@ class DB2Database implements TestableDatabase { * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located * at `$HOME/.testcontainers.properties` (create the file if it does not exist). */ - static final Db2Container db2 = new Db2Container( imageName( "icr.io", "db2_community/db2", "12.1.0.0" ) ) + static final Db2Container db2 = new Db2Container( fromDockerfile( "db2" ) ) .withUsername( DatabaseConfiguration.USERNAME ) .withPassword( DatabaseConfiguration.PASSWORD ) .withDatabaseName( DatabaseConfiguration.DB_NAME ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DockerImage.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DockerImage.java index 1b2f3f6a7..e8f40f34d 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DockerImage.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DockerImage.java @@ -5,10 +5,17 @@ */ package org.hibernate.reactive.containers; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + import org.testcontainers.utility.DockerImageName; + /** - * A utility class with methods to generate {@link DockerImageName} for testcontainers. + * A utility class with methods to generate a {@link DockerImageName} for Testcontainers. *

* Testcontainers might not work if the image required is available in multiple different registries (for example when * using podman instead of docker). @@ -17,10 +24,28 @@ */ public final class DockerImage { - public static final String DEFAULT_REGISTRY = "docker.io"; + /** + * The absolute path of the project root that we have set in Gradle. + */ + private static final String PROJECT_ROOT = System.getProperty( "hibernate.reactive.project.root" ); + + /** + * The path to the directory containing all the Dockerfile files + */ + private static final Path DOCKERFILE_DIR_PATH = Path.of( PROJECT_ROOT ).resolve( "tooling" ).resolve( "docker" ); - public static DockerImageName imageName(String image, String version) { - return imageName( DEFAULT_REGISTRY, image, version ); + /** + * Extract the image name and version from the first FROM instruction in the Dockerfile. + * Note that everything else is ignored. + */ + public static DockerImageName fromDockerfile(String databaseName) { + try { + final ImageInformation imageInformation = readFromInstruction( databaseName.toLowerCase() ); + return imageName( imageInformation.getRegistry(), imageInformation.getImage(), imageInformation.getVersion() ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } } public static DockerImageName imageName(String registry, String image, String version) { @@ -28,4 +53,74 @@ public static DockerImageName imageName(String registry, String image, String ve .parse( registry + "/" + image + ":" + version ) .asCompatibleSubstituteFor( image ); } + + private static class ImageInformation { + private final String registry; + private final String image; + private final String version; + + public ImageInformation(String fullImageInfo) { + // FullImageInfo pattern: /: + // For example: "docker.io/cockroachdb/cockroach:v24.3.13" becomes registry = "docker.io", image = "cockroachdb/cockroach", version = "v24.3.13" + final int registryEndPos = fullImageInfo.indexOf( '/' ); + final int imageEndPos = fullImageInfo.lastIndexOf( ':' ); + this.registry = fullImageInfo.substring( 0, registryEndPos ); + this.image = fullImageInfo.substring( registryEndPos + 1, imageEndPos ); + this.version = fullImageInfo.substring( imageEndPos + 1 ); + } + + public String getRegistry() { + return registry; + } + + public String getImage() { + return image; + } + + public String getVersion() { + return version; + } + + @Override + public String toString() { + return registry + "/" + image + ":" + version; + } + } + + private static Path dockerFilePath(String database) { + // Get project root from system property set by Gradle, with fallback + return DOCKERFILE_DIR_PATH.resolve( database.toLowerCase() + ".Dockerfile" ); + } + + private static ImageInformation readFromInstruction(String database) throws IOException { + return readFromInstruction( dockerFilePath( database ) ); + } + + /** + * Read a Dockerfile and extract the first FROM instruction. + * + * @param dockerfilePath path to the Dockerfile + * @return the first FROM instruction found, or empty if none found + * @throws IOException if the file cannot be read + */ + private static ImageInformation readFromInstruction(Path dockerfilePath) throws IOException { + if ( !Files.exists( dockerfilePath ) ) { + throw new FileNotFoundException( "Dockerfile not found: " + dockerfilePath ); + } + + List lines = Files.readAllLines( dockerfilePath ); + for ( String line : lines ) { + // Skip comments and empty lines + String trimmedLine = line.trim(); + if ( trimmedLine.isEmpty() || trimmedLine.startsWith( "#" ) ) { + continue; + } + + if ( trimmedLine.startsWith( "FROM " ) ) { + return new ImageInformation( trimmedLine.substring( "FROM ".length() ) ); + } + } + + throw new IOException( " Missing FROM instruction in " + dockerfilePath ); + } } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java index 8078d1b7e..d0219f34d 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java @@ -28,7 +28,7 @@ import org.testcontainers.containers.MSSQLServerContainer; -import static org.hibernate.reactive.containers.DockerImage.imageName; +import static org.hibernate.reactive.containers.DockerImage.fromDockerfile; /** * The JDBC driver syntax is: @@ -96,7 +96,7 @@ class MSSQLServerDatabase implements TestableDatabase { * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located * at `$HOME/.testcontainers.properties` (create the file if it does not exist). */ - public static final MSSQLServerContainer mssqlserver = new MSSQLServerContainer<>( imageName( "mcr.microsoft.com", "mssql/server", "2022-latest" ) ) + public static final MSSQLServerContainer mssqlserver = new MSSQLServerContainer<>( fromDockerfile( "sqlserver" ) ) .acceptLicense() .withPassword( PASSWORD ) .withReuse( true ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java index 575c5290d..e704df38c 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java @@ -6,10 +6,10 @@ package org.hibernate.reactive.containers; -import static org.hibernate.reactive.containers.DockerImage.imageName; - import org.testcontainers.containers.MariaDBContainer; +import static org.hibernate.reactive.containers.DockerImage.fromDockerfile; + class MariaDatabase extends MySQLDatabase { static MariaDatabase INSTANCE = new MariaDatabase(); @@ -21,7 +21,7 @@ class MariaDatabase extends MySQLDatabase { * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located * at `$HOME/.testcontainers.properties` (create the file if it does not exist). */ - public static final MariaDBContainer maria = new MariaDBContainer<>( imageName( "mariadb", "11.4.2" ) ) + public static final MariaDBContainer maria = new MariaDBContainer<>( fromDockerfile( "maria" ) ) .withUsername( DatabaseConfiguration.USERNAME ) .withPassword( DatabaseConfiguration.PASSWORD ) .withDatabaseName( DatabaseConfiguration.DB_NAME ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java index 9b94cf8ed..28b3803be 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java @@ -5,7 +5,7 @@ */ package org.hibernate.reactive.containers; -import static org.hibernate.reactive.containers.DockerImage.imageName; +import static org.hibernate.reactive.containers.DockerImage.fromDockerfile; import java.io.Serializable; import java.math.BigDecimal; @@ -87,7 +87,7 @@ class MySQLDatabase implements TestableDatabase { * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located * at `$HOME/.testcontainers.properties` (create the file if it does not exist). */ - public static final MySQLContainer mysql = new MySQLContainer<>( imageName( "mysql", "8.4.0") ) + public static final MySQLContainer mysql = new MySQLContainer<>( fromDockerfile( "mysql" ).asCompatibleSubstituteFor( "mysql" ) ) .withUsername( DatabaseConfiguration.USERNAME ) .withPassword( DatabaseConfiguration.PASSWORD ) .withDatabaseName( DatabaseConfiguration.DB_NAME ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java index 9ad2f8136..594ccf462 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java @@ -29,7 +29,7 @@ import org.testcontainers.containers.OracleContainer; -import static org.hibernate.reactive.containers.DockerImage.imageName; +import static org.hibernate.reactive.containers.DockerImage.fromDockerfile; /** * Connection string for Oracle thin should be something like: @@ -88,9 +88,7 @@ class OracleDatabase implements TestableDatabase { } } - public static final OracleContainer oracle = new OracleContainer( - imageName( "gvenzl/oracle-free", "23-slim-faststart" ) - .asCompatibleSubstituteFor( "gvenzl/oracle-xe" ) ) + public static final OracleContainer oracle = new OracleContainer( fromDockerfile( "oracle" ).asCompatibleSubstituteFor( "gvenzl/oracle-xe" ) ) .withUsername( DatabaseConfiguration.USERNAME ) .withPassword( DatabaseConfiguration.PASSWORD ) .withDatabaseName( DatabaseConfiguration.DB_NAME ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java index 038733e7e..4f31ea47b 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java @@ -5,8 +5,6 @@ */ package org.hibernate.reactive.containers; -import static org.hibernate.reactive.containers.DockerImage.imageName; - import java.io.Serializable; import java.math.BigDecimal; import java.math.BigInteger; @@ -30,9 +28,11 @@ import org.testcontainers.containers.PostgreSQLContainer; +import static org.hibernate.reactive.containers.DockerImage.fromDockerfile; + class PostgreSQLDatabase implements TestableDatabase { - public static PostgreSQLDatabase INSTANCE = new PostgreSQLDatabase(); + public static final PostgreSQLDatabase INSTANCE = new PostgreSQLDatabase(); private static Map, String> expectedDBTypeForClass = new HashMap<>(); @@ -87,7 +87,7 @@ class PostgreSQLDatabase implements TestableDatabase { * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located * at `$HOME/.testcontainers.properties` (create the file if it does not exist). */ - public static final PostgreSQLContainer postgresql = new PostgreSQLContainer<>( imageName( "postgres", "16.3" ) ) + public static final PostgreSQLContainer postgresql = new PostgreSQLContainer<>( fromDockerfile( "postgresql" ) ) .withUsername( DatabaseConfiguration.USERNAME ) .withPassword( DatabaseConfiguration.PASSWORD ) .withDatabaseName( DatabaseConfiguration.DB_NAME ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/delegation/ConcreteSessionDelegator.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/delegation/ConcreteSessionDelegator.java new file mode 100644 index 000000000..2c93d9226 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/delegation/ConcreteSessionDelegator.java @@ -0,0 +1,17 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.delegation; + +import org.hibernate.reactive.mutiny.Mutiny; +import org.hibernate.reactive.mutiny.delegation.MutinySessionDelegator; + +@SuppressWarnings("unused") +class ConcreteSessionDelegator extends MutinySessionDelegator { + @Override + public Mutiny.Session delegate() { + throw new UnsupportedOperationException(); + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/delegation/ConcreteStatelessSessionDelegator.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/delegation/ConcreteStatelessSessionDelegator.java new file mode 100644 index 000000000..e275d0180 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/delegation/ConcreteStatelessSessionDelegator.java @@ -0,0 +1,17 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.delegation; + +import org.hibernate.reactive.mutiny.Mutiny; +import org.hibernate.reactive.mutiny.delegation.MutinyStatelessSessionDelegator; + +@SuppressWarnings("unused") +class ConcreteStatelessSessionDelegator extends MutinyStatelessSessionDelegator { + @Override + public Mutiny.StatelessSession delegate() { + throw new UnsupportedOperationException(); + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaValidationTestBase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaValidationTestBase.java index 4a5117fee..2034704cc 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaValidationTestBase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaValidationTestBase.java @@ -6,11 +6,15 @@ package org.hibernate.reactive.schema; +import java.net.URL; + import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Configuration; import org.hibernate.reactive.BaseReactiveTest; -import org.hibernate.reactive.provider.Settings; import org.hibernate.reactive.annotations.DisabledFor; +import org.hibernate.reactive.annotations.EnabledFor; +import org.hibernate.reactive.provider.Settings; import org.hibernate.tool.schema.spi.SchemaManagementException; import org.junit.jupiter.api.AfterEach; @@ -19,6 +23,7 @@ import io.vertx.junit5.Timeout; import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; @@ -26,6 +31,7 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE; import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.GROUPED; import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.INDIVIDUALLY; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -74,6 +80,11 @@ public void before(VertxTestContext context) { Configuration createConf = constructConfiguration( "create" ); createConf.addAnnotatedClass( BasicTypesTestEntity.class ); + final URL importFileURL = Thread.currentThread() + .getContextClassLoader() + .getResource( "oracle-SchemaValidationTest.sql" ); + createConf.setProperty( AvailableSettings.JAKARTA_HBM2DDL_LOAD_SCRIPT_SOURCE, importFileURL.getFile() ); + // Make sure that the extra table is not in the db Configuration dropConf = constructConfiguration( "drop" ); dropConf.addAnnotatedClass( Extra.class ); @@ -92,6 +103,18 @@ public void after(VertxTestContext context) { closeFactory( context ); } + @Test + @Timeout(value = 10, timeUnit = MINUTES) + @EnabledFor( ORACLE ) + public void testOracleColumnTypeValidation(VertxTestContext context) { + Configuration validateConf = constructConfiguration( "validate" ); + validateConf.addAnnotatedClass( Fruit.class ); + + StandardServiceRegistryBuilder builder = new StandardServiceRegistryBuilder() + .applySettings( validateConf.getProperties() ); + test( context, setupSessionFactory( validateConf ) ); + } + // When we have created the table, the validation should pass @Test @Timeout(value = 10, timeUnit = MINUTES) @@ -139,4 +162,43 @@ public static class Extra { private String description; } + + @Entity(name = "Fruit") + public static class Fruit { + + @Id + @GeneratedValue + private Integer id; + + @Column(name = "something_name", nullable = false, updatable = false) + private String name; + + public Fruit() { + } + + public Fruit(String name) { + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "Fruit{" + id + "," + name + '}'; + } + } } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/TemporaryIdTableStrategyTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/TemporaryIdTableStrategyTest.java new file mode 100644 index 000000000..15ed24341 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/TemporaryIdTableStrategyTest.java @@ -0,0 +1,235 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.schema; + +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.stream.Stream; + +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.Configuration; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.temptable.TemporaryTable; +import org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableStrategy; +import org.hibernate.query.sqm.mutation.internal.temptable.PersistentTableStrategy; +import org.hibernate.reactive.BaseReactiveTest; +import org.hibernate.reactive.annotations.EnabledFor; +import org.hibernate.reactive.provider.Settings; +import org.hibernate.reactive.testing.SqlStatementTracker; +import org.hibernate.reactive.util.impl.CompletionStages; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.COCKROACHDB; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * Test enabling and disabling of strategies for the creation of temporary tables to store ids. + * + * @see GlobalTemporaryTableStrategy + * @see PersistentTableStrategy + */ +@Timeout(value = 10, timeUnit = MINUTES) +public class TemporaryIdTableStrategyTest extends BaseReactiveTest { + private static SqlStatementTracker sqlStatementTracker; + + final static Dialect[] dialect = new Dialect[1]; + + @Override + protected Collection> annotatedEntities() { + return Set.of( Engineer.class, Doctor.class, Person.class ); + } + + public static Stream settings() { + return Stream.of( + arguments( true, 1, true, 1 ), + arguments( true, 1, false, 0 ), + // I'm assuming Hibernate won't drop the id tables if they haven't been created + arguments( false, 0, true, 0 ), + arguments( false, 0, false, 0 ) + ); + } + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + configuration.setProperty( Settings.HBM2DDL_AUTO, "create" ); + // Collect all the logs, we are going to filter them later + sqlStatementTracker = new SqlStatementTracker( s -> true, configuration.getProperties() ); + return configuration; + } + + @Override + public CompletionStage deleteEntities(Class... entities) { + // Deleting entities is not necessary for this test + return voidFuture(); + } + + @Override + public void before(VertxTestContext context) { + // We need to start and close our own session factories for the test + } + + @AfterEach + @Override + public void after(VertxTestContext context) { + sqlStatementTracker.clear(); + super.after( context ); + } + + @Override + protected void addServices(StandardServiceRegistryBuilder builder) { + if ( sqlStatementTracker != null ) { + sqlStatementTracker.registerService( builder ); + } + } + + @ParameterizedTest(name = "Global Temporary tables - create: {0}, drop: {2}") + @MethodSource("settings") + @EnabledFor(value = ORACLE, reason = "It uses GlobalTemporaryTableStrategy by default") + public void testGlobalTemporaryTablesStrategy( + boolean enableCreateIdTables, + // Expected number of temporary tables created + int expectedTempTablesCreated, + boolean enableDropIdTables, + // Expected number of temporary tables dropped + int expectedTempTablesDropped, + VertxTestContext context) { + Configuration configuration = constructConfiguration(); + configuration.setProperty( GlobalTemporaryTableStrategy.CREATE_ID_TABLES, enableCreateIdTables ); + configuration.setProperty( GlobalTemporaryTableStrategy.DROP_ID_TABLES, enableDropIdTables ); + + testTemporaryIdTablesCreationAndDropping( configuration, expectedTempTablesCreated, expectedTempTablesDropped, context ); + } + + @ParameterizedTest(name = "Persistent tables - create: {0}, drop: {2}") + @MethodSource("settings") + @EnabledFor(value = COCKROACHDB, reason = "It uses PersistentTemporaryTableStrategy by default") + public void testPersistentTemporaryTablesStrategy( + boolean enableCreateIdTables, + // Expected number of temporary tables created + int expectedTempTablesCreated, + boolean enableDropIdTables, + // Expected number of temporary tables dropped + int expectedTempTablesDropped, + VertxTestContext context) { + + Configuration configuration = constructConfiguration(); + configuration.setProperty( PersistentTableStrategy.CREATE_ID_TABLES, enableCreateIdTables ); + configuration.setProperty( PersistentTableStrategy.DROP_ID_TABLES, enableDropIdTables ); + + testTemporaryIdTablesCreationAndDropping( configuration, expectedTempTablesCreated, expectedTempTablesDropped, context ); + } + + private void testTemporaryIdTablesCreationAndDropping( + Configuration configure, + int expectedTempTablesCreated, + int expectedTempTablesDropped, + VertxTestContext context) { + test( context, setupSessionFactory( configure ) + .thenAccept( v -> { + dialect[0] = getDialect(); + assertThat( commandsCount( dialect[0].getTemporaryTableCreateCommand() ) ) + .as( "Unexpected number of temporary tables for ids CREATED" ) + .isEqualTo( expectedTempTablesCreated ); + sqlStatementTracker.clear(); + } ) + // to ensure the factory is always closed even in case of exceptions + .handle( CompletionStages::handle ) + .thenCompose( this::closeFactory ) + .thenAccept( v -> assertThat( commandsCount( dialect[0].getTemporaryTableDropCommand() ) ) + .as( "Unexpected number of temporary tables for ids DROPPED" ) + .isEqualTo( expectedTempTablesDropped ) ) + ); + } + + // Always try to close the factory without losing the original error (if there was one) + private CompletionStage closeFactory(CompletionStages.CompletionStageHandler handler) { + return factoryManager.stop() + .handle( CompletionStages::handle ) + .thenCompose( factoryHandler -> handler + .getResultAsCompletionStage() + // When there's already an exception, we don't care about errors closing the factory + .thenCompose( factoryHandler::getResultAsCompletionStage ) ); + } + + private static long commandsCount(String temporaryTableCommand) { + return sqlStatementTracker.getLoggedQueries().stream() + .filter( q -> q.startsWith( temporaryTableCommand ) && q.contains( TemporaryTable.ID_TABLE_PREFIX ) ) + .count(); + } + + @Entity(name = "Person") + @Inheritance(strategy = InheritanceType.JOINED) + public static class Person { + + @Id + @GeneratedValue + private Long id; + + private String name; + + private boolean employed; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isEmployed() { + return employed; + } + + public void setEmployed(boolean employed) { + this.employed = employed; + } + } + + @Entity(name = "Doctor") + public static class Doctor extends Person { + } + + @Entity(name = "Engineer") + public static class Engineer extends Person { + + private boolean fellow; + + public boolean isFellow() { + return fellow; + } + + public void setFellow(boolean fellow) { + this.fellow = fellow; + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/testing/SessionFactoryManager.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/testing/SessionFactoryManager.java index 8262b7c22..be4362a02 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/testing/SessionFactoryManager.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/testing/SessionFactoryManager.java @@ -5,20 +5,13 @@ */ package org.hibernate.reactive.testing; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.CompletionStage; import java.util.function.Supplier; import org.hibernate.SessionFactory; import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.metamodel.spi.MappingMetamodelImplementor; -import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.query.sqm.mutation.internal.temptable.PersistentTableStrategy; import org.hibernate.reactive.pool.ReactiveConnectionPool; -import org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactivePersistentTableStrategy; -import static org.hibernate.reactive.util.impl.CompletionStages.loop; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; /** @@ -60,24 +53,8 @@ public ReactiveConnectionPool getReactiveConnectionPool() { public CompletionStage stop() { CompletionStage releasedStage = voidFuture(); if ( sessionFactory != null && sessionFactory.isOpen() ) { - SessionFactoryImplementor sessionFactoryImplementor = sessionFactory.unwrap( SessionFactoryImplementor.class ); - MappingMetamodelImplementor mappingMetamodel = sessionFactoryImplementor - .getRuntimeMetamodels() - .getMappingMetamodel(); - final List reactiveStrategies = new ArrayList<>(); - mappingMetamodel.forEachEntityDescriptor( - entityPersister -> addPersistentTableStrategy( reactiveStrategies, entityPersister ) - ); - if ( !reactiveStrategies.isEmpty() ) { - releasedStage = loop( reactiveStrategies, strategy -> { - ( (PersistentTableStrategy) strategy ) - .release( sessionFactory.unwrap( SessionFactoryImplementor.class ), null ); - return strategy.getDropTableActionStage(); - } ); - - releasedStage = releasedStage - .whenComplete( (unused, throwable) -> sessionFactory.close() ); - } + releasedStage = releasedStage + .whenComplete( (unused, throwable) -> sessionFactory.close() ); } return releasedStage .thenCompose( unused -> { @@ -93,13 +70,4 @@ public CompletionStage stop() { return closeFuture; } ); } - - private void addPersistentTableStrategy(List reactiveStrategies, EntityPersister entityPersister) { - if ( entityPersister.getSqmMultiTableMutationStrategy() instanceof ReactivePersistentTableStrategy ) { - reactiveStrategies.add( (ReactivePersistentTableStrategy) entityPersister.getSqmMultiTableMutationStrategy() ); - } - if ( entityPersister.getSqmMultiTableInsertStrategy() instanceof ReactivePersistentTableStrategy ) { - reactiveStrategies.add( (ReactivePersistentTableStrategy) entityPersister.getSqmMultiTableInsertStrategy() ); - } - } } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/AutoZonedTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/AutoZonedTest.java index a799f5d6b..eb502bdb1 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/AutoZonedTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/AutoZonedTest.java @@ -19,6 +19,7 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.List; @@ -29,8 +30,11 @@ import static org.hibernate.cfg.AvailableSettings.TIMEZONE_DEFAULT_STORAGE; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; import static org.hibernate.reactive.testing.ReactiveAssertions.assertWithTruncationThat; -import static org.hibernate.type.descriptor.DateTimeUtils.roundToDefaultPrecision; +import static org.hibernate.type.descriptor.DateTimeUtils.adjustToDefaultPrecision; +/** + * Test adapted from {@link org.hibernate.orm.test.timezones.AutoZonedTest} + */ @Timeout(value = 10, timeUnit = MINUTES) @DisabledFor(value = DB2, reason = "Exception: IllegalStateException: Needed to have 6 in buffer but only had 0") public class AutoZonedTest extends BaseReactiveTest { @@ -48,8 +52,16 @@ protected void setProperties(Configuration configuration) { @Test public void test(VertxTestContext context) { - ZonedDateTime nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of( "CET" ) ); - OffsetDateTime nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours( 3 ) ); + final ZonedDateTime nowZoned; + final OffsetDateTime nowOffset; + if ( getDialect().getDefaultTimestampPrecision() == 6 ) { + nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of("CET") ).truncatedTo( ChronoUnit.MICROS ); + nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours(3) ).truncatedTo( ChronoUnit.MICROS ); + } + else { + nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of("CET") ); + nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours(3) ); + } test( context, getSessionFactory() .withTransaction( s -> { Zoned z = new Zoned(); @@ -60,10 +72,10 @@ public void test(VertxTestContext context) { .thenCompose( zid -> openSession() .thenCompose( s -> s.find( Zoned.class, zid ) .thenAccept( z -> { - assertWithTruncationThat( roundToDefaultPrecision( z.zonedDateTime.toInstant(), getDialect() ) ) - .isEqualTo( roundToDefaultPrecision( nowZoned.toInstant(), getDialect() ) ); - assertWithTruncationThat( roundToDefaultPrecision( z.offsetDateTime.toInstant(), getDialect() ) ) - .isEqualTo( roundToDefaultPrecision( nowOffset.toInstant(), getDialect() ) ); + assertWithTruncationThat( adjustToDefaultPrecision( z.zonedDateTime.toInstant(), getDialect() ) ) + .isEqualTo( adjustToDefaultPrecision( nowZoned.toInstant(), getDialect() ) ); + assertWithTruncationThat( adjustToDefaultPrecision( z.offsetDateTime.toInstant(), getDialect() ) ) + .isEqualTo( adjustToDefaultPrecision( nowOffset.toInstant(), getDialect() ) ); assertThat( z.zonedDateTime.toOffsetDateTime().getOffset() ) .isEqualTo( nowZoned.toOffsetDateTime().getOffset() ); assertThat( z.offsetDateTime.getOffset() ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/ColumnZonedTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/ColumnZonedTest.java index f10e394e1..6e33da4f4 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/ColumnZonedTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/ColumnZonedTest.java @@ -9,6 +9,7 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.List; @@ -29,8 +30,11 @@ import static org.hibernate.cfg.AvailableSettings.TIMEZONE_DEFAULT_STORAGE; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; import static org.hibernate.reactive.testing.ReactiveAssertions.assertWithTruncationThat; -import static org.hibernate.type.descriptor.DateTimeUtils.roundToDefaultPrecision; +import static org.hibernate.type.descriptor.DateTimeUtils.adjustToDefaultPrecision; +/** + * Test adapted from {@link org.hibernate.orm.test.timezones.ColumnZonedTest} + */ @Timeout(value = 10, timeUnit = MINUTES) @DisabledFor(value = DB2, reason = "java.sql.SQLException: An error occurred with a DB2 operation, SQLCODE=-180 SQLSTATE=22007") public class ColumnZonedTest extends BaseReactiveTest { @@ -48,8 +52,16 @@ protected void setProperties(Configuration configuration) { @Test public void test(VertxTestContext context) { - ZonedDateTime nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of( "CET" ) ); - OffsetDateTime nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours( 3 ) ); + final ZonedDateTime nowZoned; + final OffsetDateTime nowOffset; + if ( getDialect().getDefaultTimestampPrecision() == 6 ) { + nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of("CET") ).truncatedTo( ChronoUnit.MICROS ); + nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours(3) ).truncatedTo( ChronoUnit.MICROS ); + } + else { + nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of("CET") ); + nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours(3) ); + } test( context, getSessionFactory() .withTransaction( s -> { Zoned z = new Zoned(); @@ -60,10 +72,10 @@ public void test(VertxTestContext context) { .thenCompose( zid -> openSession() .thenCompose( s -> s.find( Zoned.class, zid ) .thenAccept( z -> { - assertWithTruncationThat( roundToDefaultPrecision( z.zonedDateTime.toInstant(), getDialect() ) ) - .isEqualTo( roundToDefaultPrecision( nowZoned.toInstant(), getDialect() ) ); - assertWithTruncationThat( roundToDefaultPrecision( z.offsetDateTime.toInstant(), getDialect() ) ) - .isEqualTo( roundToDefaultPrecision( nowOffset.toInstant(), getDialect() ) ); + assertWithTruncationThat( adjustToDefaultPrecision( z.zonedDateTime.toInstant(), getDialect() ) ) + .isEqualTo( adjustToDefaultPrecision( nowZoned.toInstant(), getDialect() ) ); + assertWithTruncationThat( adjustToDefaultPrecision( z.offsetDateTime.toInstant(), getDialect() ) ) + .isEqualTo( adjustToDefaultPrecision( nowOffset.toInstant(), getDialect() ) ); assertThat( z.zonedDateTime.toOffsetDateTime().getOffset() ) .isEqualTo( nowZoned.toOffsetDateTime().getOffset() ); assertThat( z.offsetDateTime.getOffset() ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/DefaultZonedTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/DefaultZonedTest.java index 6752b7219..4ce4dbea8 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/DefaultZonedTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/DefaultZonedTest.java @@ -9,6 +9,7 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.List; @@ -28,8 +29,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; import static org.hibernate.reactive.testing.ReactiveAssertions.assertWithTruncationThat; -import static org.hibernate.type.descriptor.DateTimeUtils.roundToDefaultPrecision; +import static org.hibernate.type.descriptor.DateTimeUtils.adjustToDefaultPrecision; +/** + * Test adapted from {@link org.hibernate.orm.test.timezones.DefaultZonedTest} + */ @Timeout(value = 10, timeUnit = MINUTES) @DisabledFor(value = DB2, reason = "Exception: IllegalStateException: Needed to have 6 in buffer but only had 0") public class DefaultZonedTest extends BaseReactiveTest { @@ -41,8 +45,16 @@ protected Collection> annotatedEntities() { @Test public void test(VertxTestContext context) { - ZonedDateTime nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of( "CET" ) ); - OffsetDateTime nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours( 3 ) ); + final ZonedDateTime nowZoned; + final OffsetDateTime nowOffset; + if ( getDialect().getDefaultTimestampPrecision() == 6 ) { + nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of("CET") ).truncatedTo( ChronoUnit.MICROS ); + nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours(3) ).truncatedTo( ChronoUnit.MICROS ); + } + else { + nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of("CET") ); + nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours(3) ); + } test( context, getSessionFactory() .withTransaction( s -> { Zoned z = new Zoned(); @@ -53,10 +65,10 @@ public void test(VertxTestContext context) { .thenCompose( zid -> openSession() .thenCompose( s -> s.find( Zoned.class, zid ) .thenAccept( z -> { - assertWithTruncationThat( roundToDefaultPrecision( z.zonedDateTime.toInstant(), getDialect() ) ) - .isEqualTo( roundToDefaultPrecision( nowZoned.toInstant(), getDialect() ) ); - assertWithTruncationThat( roundToDefaultPrecision( z.offsetDateTime.toInstant(), getDialect() ) ) - .isEqualTo( roundToDefaultPrecision( nowOffset.toInstant(), getDialect() ) ); + assertWithTruncationThat( adjustToDefaultPrecision( z.zonedDateTime.toInstant(), getDialect() ) ) + .isEqualTo( adjustToDefaultPrecision( nowZoned.toInstant(), getDialect() ) ); + assertWithTruncationThat( adjustToDefaultPrecision( z.offsetDateTime.toInstant(), getDialect() ) ) + .isEqualTo( adjustToDefaultPrecision( nowOffset.toInstant(), getDialect() ) ); if ( getDialect().getTimeZoneSupport() == TimeZoneSupport.NATIVE ) { assertThat( z.zonedDateTime.toOffsetDateTime().getOffset() ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/JDBCTimeZoneZonedTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/JDBCTimeZoneZonedTest.java index 5b928c217..71966bfc1 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/JDBCTimeZoneZonedTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/JDBCTimeZoneZonedTest.java @@ -10,6 +10,7 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.List; @@ -31,8 +32,11 @@ import static org.hibernate.cfg.AvailableSettings.TIMEZONE_DEFAULT_STORAGE; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; import static org.hibernate.reactive.testing.ReactiveAssertions.assertWithTruncationThat; -import static org.hibernate.type.descriptor.DateTimeUtils.roundToDefaultPrecision; +import static org.hibernate.type.descriptor.DateTimeUtils.adjustToDefaultPrecision; +/** + * Test adapted from {@link org.hibernate.orm.test.timezones.JDBCTimeZoneZonedTest} + */ @Timeout(value = 10, timeUnit = MINUTES) @DisabledFor(value = DB2, reason = "Exception: IllegalStateException: Needed to have 6 in buffer but only had 0") public class JDBCTimeZoneZonedTest extends BaseReactiveTest { @@ -51,8 +55,16 @@ protected void setProperties(Configuration configuration) { @Test public void test(VertxTestContext context) { - ZonedDateTime nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of( "CET" ) ); - OffsetDateTime nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours( 3 ) ); + final ZonedDateTime nowZoned; + final OffsetDateTime nowOffset; + if ( getDialect().getDefaultTimestampPrecision() == 6 ) { + nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of("CET") ).truncatedTo( ChronoUnit.MICROS ); + nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours(3) ).truncatedTo( ChronoUnit.MICROS ); + } + else { + nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of("CET") ); + nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours(3) ); + } test( context, getSessionFactory() .withTransaction( s -> { Zoned z = new Zoned(); @@ -63,11 +75,11 @@ public void test(VertxTestContext context) { .thenCompose( zid -> openSession() .thenCompose( s -> s.find( Zoned.class, zid ) .thenAccept( z -> { - assertWithTruncationThat( roundToDefaultPrecision( z.zonedDateTime.toInstant(), getDialect() ) ) - .isEqualTo( roundToDefaultPrecision( nowZoned.toInstant(), getDialect() ) ); + assertWithTruncationThat( adjustToDefaultPrecision( z.zonedDateTime.toInstant(), getDialect() ) ) + .isEqualTo( adjustToDefaultPrecision( nowZoned.toInstant(), getDialect() ) ); - assertWithTruncationThat( roundToDefaultPrecision( z.offsetDateTime.toInstant(), getDialect() ) ) - .isEqualTo( roundToDefaultPrecision( nowOffset.toInstant(), getDialect() ) ); + assertWithTruncationThat( adjustToDefaultPrecision( z.offsetDateTime.toInstant(), getDialect() ) ) + .isEqualTo( adjustToDefaultPrecision( nowOffset.toInstant(), getDialect() ) ); ZoneId systemZone = ZoneId.systemDefault(); ZoneOffset systemOffset = systemZone.getRules().getOffset( Instant.now() ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/PassThruZonedTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/PassThruZonedTest.java index 988db4a58..3ce9f4256 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/PassThruZonedTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/PassThruZonedTest.java @@ -10,6 +10,7 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.List; @@ -30,8 +31,11 @@ import static org.hibernate.cfg.AvailableSettings.TIMEZONE_DEFAULT_STORAGE; import static org.hibernate.reactive.testing.ReactiveAssertions.assertWithTruncationThat; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; -import static org.hibernate.type.descriptor.DateTimeUtils.roundToDefaultPrecision; +import static org.hibernate.type.descriptor.DateTimeUtils.adjustToDefaultPrecision; +/** + * Test adapted from {@link org.hibernate.orm.test.timezones.PassThruZonedTest} + */ @Timeout(value = 10, timeUnit = MINUTES) @DisabledFor(value = DB2, reason = "Exception: SQLException: An error occurred with a DB2 operation, SQLCODE=-180 SQLSTATE=22007") public class PassThruZonedTest extends BaseReactiveTest { @@ -49,8 +53,16 @@ protected void setProperties(Configuration configuration) { @Test public void test(VertxTestContext context) { - ZonedDateTime nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of( "CET" ) ); - OffsetDateTime nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours( 3 ) ); + final ZonedDateTime nowZoned; + final OffsetDateTime nowOffset; + if ( getDialect().getDefaultTimestampPrecision() == 6 ) { + nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of("CET") ).truncatedTo( ChronoUnit.MICROS ); + nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours(3) ).truncatedTo( ChronoUnit.MICROS ); + } + else { + nowZoned = ZonedDateTime.now().withZoneSameInstant( ZoneId.of("CET") ); + nowOffset = OffsetDateTime.now().withOffsetSameInstant( ZoneOffset.ofHours(3) ); + } test( context, getSessionFactory() .withTransaction( s -> { Zoned z = new Zoned(); @@ -61,10 +73,10 @@ public void test(VertxTestContext context) { .thenCompose( zid -> openSession() .thenCompose( s -> s.find( Zoned.class, zid ) .thenAccept( z -> { - assertWithTruncationThat( roundToDefaultPrecision( z.zonedDateTime.toInstant(), getDialect() ) ) - .isEqualTo( roundToDefaultPrecision( nowZoned.toInstant(), getDialect() ) ); - assertWithTruncationThat( roundToDefaultPrecision( z.offsetDateTime.toInstant(), getDialect() ) ) - .isEqualTo( roundToDefaultPrecision( nowOffset.toInstant(), getDialect() ) ); + assertWithTruncationThat( adjustToDefaultPrecision( z.zonedDateTime.toInstant(), getDialect() ) ) + .isEqualTo( adjustToDefaultPrecision( nowZoned.toInstant(), getDialect() ) ); + assertWithTruncationThat( adjustToDefaultPrecision( z.offsetDateTime.toInstant(), getDialect() ) ) + .isEqualTo( adjustToDefaultPrecision( nowOffset.toInstant(), getDialect() ) ); ZoneId systemZone = ZoneId.systemDefault(); ZoneOffset systemOffset = systemZone.getRules().getOffset( Instant.now() ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/UTCNormalizedZonedTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/UTCNormalizedZonedTest.java index 9943a0f97..474f5df7b 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/UTCNormalizedZonedTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/UTCNormalizedZonedTest.java @@ -29,7 +29,7 @@ import static org.hibernate.cfg.AvailableSettings.TIMEZONE_DEFAULT_STORAGE; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; import static org.hibernate.reactive.testing.ReactiveAssertions.assertWithTruncationThat; -import static org.hibernate.type.descriptor.DateTimeUtils.roundToDefaultPrecision; +import static org.hibernate.type.descriptor.DateTimeUtils.adjustToDefaultPrecision; @Timeout(value = 10, timeUnit = MINUTES) @DisabledFor(value = DB2, reason = "Exception: IllegalStateException: Needed to have 6 in buffer but only had 0") @@ -60,10 +60,10 @@ public void test(VertxTestContext context) { .thenCompose( zid -> openSession() .thenCompose( s -> s.find( Zoned.class, zid ) .thenAccept( z -> { - assertWithTruncationThat( roundToDefaultPrecision( z.zonedDateTime.toInstant(), getDialect() ) ) - .isEqualTo( roundToDefaultPrecision( nowZoned.toInstant(), getDialect() ) ); - assertWithTruncationThat( roundToDefaultPrecision( z.offsetDateTime.toInstant(), getDialect() ) ) - .isEqualTo( roundToDefaultPrecision( nowOffset.toInstant(), getDialect() ) ); + assertWithTruncationThat( adjustToDefaultPrecision( z.zonedDateTime.toInstant(), getDialect() ) ) + .isEqualTo( adjustToDefaultPrecision( nowZoned.toInstant(), getDialect() ) ); + assertWithTruncationThat( adjustToDefaultPrecision( z.offsetDateTime.toInstant(), getDialect() ) ) + .isEqualTo( adjustToDefaultPrecision( nowOffset.toInstant(), getDialect() ) ); assertThat( z.offsetDateTime.getOffset() ).isEqualTo( ZoneId.of( "Z" ) ); assertThat( z.zonedDateTime.getZone() ).isEqualTo( ZoneOffset.ofHours( 0 ) ); } ) diff --git a/hibernate-reactive-core/src/test/resources/oracle-SchemaValidationTest.sql b/hibernate-reactive-core/src/test/resources/oracle-SchemaValidationTest.sql new file mode 100644 index 000000000..69a01b357 --- /dev/null +++ b/hibernate-reactive-core/src/test/resources/oracle-SchemaValidationTest.sql @@ -0,0 +1,12 @@ +-- Import file for testing schema validation in SchemaValidationTest +drop table if exists Fruit cascade constraints +drop sequence if exists Fruit_SEQ + +-- Create the table manually, so that we can check if the validation succeeds +create sequence fruit_seq start with 1 increment by 50; +create table Fruit (id number(10,0) not null, something_name nvarchar2(20) not null, primary key (id)) + +INSERT INTO fruit(id, something_name) VALUES (1, 'Cherry'); +INSERT INTO fruit(id, something_name) VALUES (2, 'Apple'); +INSERT INTO fruit(id, something_name) VALUES (3, 'Banana'); +ALTER SEQUENCE fruit_seq RESTART start with 4; \ No newline at end of file diff --git a/integration-tests/bytecode-enhancements-it/build.gradle b/integration-tests/bytecode-enhancements-it/build.gradle index 177deae9c..cd9a23e52 100644 --- a/integration-tests/bytecode-enhancements-it/build.gradle +++ b/integration-tests/bytecode-enhancements-it/build.gradle @@ -10,37 +10,32 @@ buildscript { } plugins { - id "org.hibernate.orm" version "${hibernateOrmGradlePluginVersion}" + alias(libs.plugins.org.hibernate.orm) } description = 'Bytecode enhancements integration tests' -ext { - log4jVersion = '2.20.0' - assertjVersion = '3.26.3' -} - dependencies { implementation project(':hibernate-reactive-core') // JPA metamodel generation for criteria queries (optional) - annotationProcessor "org.hibernate.orm:hibernate-jpamodelgen:${hibernateOrmVersion}" + annotationProcessor(libs.org.hibernate.orm.hibernate.jpamodelgen) // Testing on one database should be enough - runtimeOnly "io.vertx:vertx-pg-client:${vertxSqlClientVersion}" + runtimeOnly(libs.io.vertx.vertx.pg.client) // Allow authentication to PostgreSQL using SCRAM: - runtimeOnly 'com.ongres.scram:client:2.1' + runtimeOnly(libs.com.ongres.scram.client) // logging - runtimeOnly "org.apache.logging.log4j:log4j-core:${log4jVersion}" + runtimeOnly(libs.org.apache.logging.log4j.log4j.core) // Testcontainers - testImplementation "org.testcontainers:postgresql:${testcontainersVersion}" + testImplementation(libs.org.testcontainers.postgresql) // Testing - testImplementation "org.assertj:assertj-core:${assertjVersion}" - testImplementation "io.vertx:vertx-junit5:${vertxSqlClientVersion}" + testImplementation(libs.org.assertj.assertj.core) + testImplementation(libs.io.vertx.vertx.junit5) } // Optional: enable the bytecode enhancements @@ -113,4 +108,4 @@ tasks.addRule( "Pattern testDb" ) { String taskName -> } } -} \ No newline at end of file +} diff --git a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/BaseReactiveIT.java b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/BaseReactiveIT.java index 72ad3273d..8f6081a8a 100644 --- a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/BaseReactiveIT.java +++ b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/BaseReactiveIT.java @@ -52,9 +52,7 @@ public abstract class BaseReactiveIT { // These properties are in DatabaseConfiguration in core public static final boolean USE_DOCKER = Boolean.getBoolean( "docker" ); - public static final DockerImageName IMAGE_NAME = DockerImageName - .parse( "docker.io/postgres:16.3" ) - .asCompatibleSubstituteFor( "postgres" ); + public static final DockerImageName IMAGE_NAME = DockerImage.fromDockerfile( "postgresql" ); public static final String USERNAME = "hreact"; public static final String PASSWORD = "hreact"; diff --git a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/DockerImage.java b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/DockerImage.java new file mode 100644 index 000000000..b5621249d --- /dev/null +++ b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/DockerImage.java @@ -0,0 +1,125 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.it; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.testcontainers.utility.DockerImageName; + +/** + * A utility class with methods to generate a {@link DockerImageName} for Testcontainers. + *

+ * Testcontainers might not work if the image required is available in multiple different registries (for example when + * using podman instead of docker). + * These methods make sure to pick a registry as default. + *

+ */ +public final class DockerImage { + + /** + * The absolute path of the project root that we have set in Gradle. + */ + private static final String PROJECT_ROOT = System.getProperty( "hibernate.reactive.project.root" ); + + /** + * The path to the directory containing all the Dockerfile files + */ + private static final Path DOCKERFILE_DIR_PATH = Path.of( PROJECT_ROOT ).resolve( "tooling" ).resolve( "docker" ); + + /** + * Extract the image name and version from the first FROM instruction in the Dockerfile. + * Note that everything else is ignored. + */ + public static DockerImageName fromDockerfile(String databaseName) { + try { + final ImageInformation imageInformation = readFromInstruction( databaseName.toLowerCase() ); + return imageName( imageInformation.getRegistry(), imageInformation.getImage(), imageInformation.getVersion() ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } + } + + public static DockerImageName imageName(String registry, String image, String version) { + return DockerImageName + .parse( registry + "/" + image + ":" + version ) + .asCompatibleSubstituteFor( image ); + } + + private static class ImageInformation { + private final String registry; + private final String image; + private final String version; + + public ImageInformation(String fullImageInfo) { + // FullImageInfo pattern: /: + // For example: "docker.io/cockroachdb/cockroach:v24.3.13" becomes registry = "docker.io", image = "cockroachdb/cockroach", version = "v24.3.13" + final int registryEndPos = fullImageInfo.indexOf( '/' ); + final int imageEndPos = fullImageInfo.lastIndexOf( ':' ); + this.registry = fullImageInfo.substring( 0, registryEndPos ); + this.image = fullImageInfo.substring( registryEndPos + 1, imageEndPos ); + this.version = fullImageInfo.substring( imageEndPos + 1 ); + } + + public String getRegistry() { + return registry; + } + + public String getImage() { + return image; + } + + public String getVersion() { + return version; + } + + @Override + public String toString() { + return registry + "/" + image + ":" + version; + } + } + + private static Path dockerFilePath(String database) { + // Get project root from system property set by Gradle, with fallback + return DOCKERFILE_DIR_PATH.resolve( database.toLowerCase() + ".Dockerfile" ); + } + + private static ImageInformation readFromInstruction(String database) throws IOException { + return readFromInstruction( dockerFilePath( database ) ); + } + + /** + * Read a Dockerfile and extract the first FROM instruction. + * + * @param dockerfilePath path to the Dockerfile + * @return the first FROM instruction found, or empty if none found + * @throws IOException if the file cannot be read + */ + private static ImageInformation readFromInstruction(Path dockerfilePath) throws IOException { + if ( !Files.exists( dockerfilePath ) ) { + throw new FileNotFoundException( "Dockerfile not found: " + dockerfilePath ); + } + + List lines = Files.readAllLines( dockerfilePath ); + for ( String line : lines ) { + // Skip comments and empty lines + String trimmedLine = line.trim(); + if ( trimmedLine.isEmpty() || trimmedLine.startsWith( "#" ) ) { + continue; + } + + if ( trimmedLine.startsWith( "FROM " ) ) { + return new ImageInformation( trimmedLine.substring( "FROM ".length() ) ); + } + } + + throw new IOException( " Missing FROM instruction in " + dockerfilePath ); + } +} diff --git a/integration-tests/hibernate-validator-postgres-it/build.gradle b/integration-tests/hibernate-validator-postgres-it/build.gradle index 017a52919..61500ef81 100644 --- a/integration-tests/hibernate-validator-postgres-it/build.gradle +++ b/integration-tests/hibernate-validator-postgres-it/build.gradle @@ -10,39 +10,34 @@ buildscript { } plugins { - id "org.hibernate.orm" version "${hibernateOrmGradlePluginVersion}" + alias(libs.plugins.org.hibernate.orm) } description = 'Quarkus QE integration tests' -ext { - log4jVersion = '2.20.0' - assertjVersion = '3.26.3' -} - dependencies { implementation project(':hibernate-reactive-core') - implementation "org.hibernate.validator:hibernate-validator:8.0.1.Final" - runtimeOnly 'org.glassfish.expressly:expressly:5.0.0' + implementation(libs.org.hibernate.validator.hibernate.validator) + runtimeOnly(libs.org.glassfish.expressly.expressly) // JPA metamodel generation for criteria queries (optional) - annotationProcessor "org.hibernate.orm:hibernate-jpamodelgen:${hibernateOrmVersion}" + annotationProcessor(libs.org.hibernate.orm.hibernate.jpamodelgen) // Testing on one database should be enough - runtimeOnly "io.vertx:vertx-pg-client:${vertxSqlClientVersion}" + runtimeOnly(libs.io.vertx.vertx.pg.client) // Allow authentication to PostgreSQL using SCRAM: - runtimeOnly 'com.ongres.scram:client:2.1' + runtimeOnly(libs.com.ongres.scram.client) // logging - runtimeOnly "org.apache.logging.log4j:log4j-core:${log4jVersion}" + runtimeOnly(libs.org.apache.logging.log4j.log4j.core) // Testcontainers - testImplementation "org.testcontainers:postgresql:${testcontainersVersion}" + testImplementation(libs.org.testcontainers.postgresql) // Testing - testImplementation "org.assertj:assertj-core:${assertjVersion}" - testImplementation "io.vertx:vertx-junit5:${vertxSqlClientVersion}" + testImplementation(libs.org.assertj.assertj.core) + testImplementation(libs.io.vertx.vertx.junit5) } // Optional: enable the bytecode enhancements @@ -116,4 +111,4 @@ tasks.addRule( "Pattern testDb" ) { String taskName -> } } -} \ No newline at end of file +} diff --git a/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/BaseReactiveIT.java b/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/BaseReactiveIT.java index 7272f13f2..fcefdaa84 100644 --- a/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/BaseReactiveIT.java +++ b/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/BaseReactiveIT.java @@ -52,9 +52,7 @@ public abstract class BaseReactiveIT { // These properties are in DatabaseConfiguration in core public static final boolean USE_DOCKER = Boolean.getBoolean( "docker" ); - public static final DockerImageName IMAGE_NAME = DockerImageName - .parse( "docker.io/postgres:16.3" ) - .asCompatibleSubstituteFor( "postgres" ); + public static final DockerImageName IMAGE_NAME = DockerImage.fromDockerfile( "postgresql" ); public static final String USERNAME = "hreact"; public static final String PASSWORD = "hreact"; diff --git a/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/DockerImage.java b/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/DockerImage.java new file mode 100644 index 000000000..7770da76c --- /dev/null +++ b/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/DockerImage.java @@ -0,0 +1,125 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.it.quarkus.qe.database; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.testcontainers.utility.DockerImageName; + +/** + * A utility class with methods to generate a {@link DockerImageName} for Testcontainers. + *

+ * Testcontainers might not work if the image required is available in multiple different registries (for example when + * using podman instead of docker). + * These methods make sure to pick a registry as default. + *

+ */ +public final class DockerImage { + + /** + * The absolute path of the project root that we have set in Gradle. + */ + private static final String PROJECT_ROOT = System.getProperty( "hibernate.reactive.project.root" ); + + /** + * The path to the directory containing all the Dockerfile files + */ + private static final Path DOCKERFILE_DIR_PATH = Path.of( PROJECT_ROOT ).resolve( "tooling" ).resolve( "docker" ); + + /** + * Extract the image name and version from the first FROM instruction in the Dockerfile. + * Note that everything else is ignored. + */ + public static DockerImageName fromDockerfile(String databaseName) { + try { + final ImageInformation imageInformation = readFromInstruction( databaseName.toLowerCase() ); + return imageName( imageInformation.getRegistry(), imageInformation.getImage(), imageInformation.getVersion() ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } + } + + public static DockerImageName imageName(String registry, String image, String version) { + return DockerImageName + .parse( registry + "/" + image + ":" + version ) + .asCompatibleSubstituteFor( image ); + } + + private static class ImageInformation { + private final String registry; + private final String image; + private final String version; + + public ImageInformation(String fullImageInfo) { + // FullImageInfo pattern: /: + // For example: "docker.io/cockroachdb/cockroach:v24.3.13" becomes registry = "docker.io", image = "cockroachdb/cockroach", version = "v24.3.13" + final int registryEndPos = fullImageInfo.indexOf( '/' ); + final int imageEndPos = fullImageInfo.lastIndexOf( ':' ); + this.registry = fullImageInfo.substring( 0, registryEndPos ); + this.image = fullImageInfo.substring( registryEndPos + 1, imageEndPos ); + this.version = fullImageInfo.substring( imageEndPos + 1 ); + } + + public String getRegistry() { + return registry; + } + + public String getImage() { + return image; + } + + public String getVersion() { + return version; + } + + @Override + public String toString() { + return registry + "/" + image + ":" + version; + } + } + + private static Path dockerFilePath(String database) { + // Get project root from system property set by Gradle, with fallback + return DOCKERFILE_DIR_PATH.resolve( database.toLowerCase() + ".Dockerfile" ); + } + + private static ImageInformation readFromInstruction(String database) throws IOException { + return readFromInstruction( dockerFilePath( database ) ); + } + + /** + * Read a Dockerfile and extract the first FROM instruction. + * + * @param dockerfilePath path to the Dockerfile + * @return the first FROM instruction found, or empty if none found + * @throws IOException if the file cannot be read + */ + private static ImageInformation readFromInstruction(Path dockerfilePath) throws IOException { + if ( !Files.exists( dockerfilePath ) ) { + throw new FileNotFoundException( "Dockerfile not found: " + dockerfilePath ); + } + + List lines = Files.readAllLines( dockerfilePath ); + for ( String line : lines ) { + // Skip comments and empty lines + String trimmedLine = line.trim(); + if ( trimmedLine.isEmpty() || trimmedLine.startsWith( "#" ) ) { + continue; + } + + if ( trimmedLine.startsWith( "FROM " ) ) { + return new ImageInformation( trimmedLine.substring( "FROM ".length() ) ); + } + } + + throw new IOException( " Missing FROM instruction in " + dockerfilePath ); + } +} diff --git a/integration-tests/techempower-postgres-it/build.gradle b/integration-tests/techempower-postgres-it/build.gradle index 925aedfd8..edfc4ae4d 100644 --- a/integration-tests/techempower-postgres-it/build.gradle +++ b/integration-tests/techempower-postgres-it/build.gradle @@ -11,37 +11,25 @@ buildscript { description = 'TechEmpower integration tests' -ext { - jacksonDatabindVersion = '2.15.2' - jbossLoggingVersion = '3.5.0.Final' - assertjVersion = '3.26.3' - vertxWebVersion = project.hasProperty( 'vertxWebVersion' ) - ? project.property( 'vertxWebVersion' ) - : vertxSqlClientVersion - vertxWebClientVersion = project.hasProperty( 'vertxWebClientVersion' ) - ? project.property( 'vertxWebClientVersion' ) - : vertxSqlClientVersion -} - dependencies { implementation project( ':hibernate-reactive-core' ) - implementation "io.vertx:vertx-web:${vertxWebVersion}" - implementation "io.vertx:vertx-web-client:${vertxWebClientVersion}" + implementation(libs.io.vertx.vertx.web) + implementation(libs.io.vertx.vertx.web.client) - runtimeOnly "io.vertx:vertx-pg-client:${vertxSqlClientVersion}" + runtimeOnly(libs.io.vertx.vertx.pg.client) // The Pg client requires this dependency - runtimeOnly "com.ongres.scram:client:2.1" - runtimeOnly "com.fasterxml.jackson.core:jackson-databind:${jacksonDatabindVersion}" + runtimeOnly(libs.com.ongres.scram.client) + runtimeOnly(libs.com.fasterxml.jackson.core.jackson.databind) // logging - implementation "org.jboss.logging:jboss-logging:${jbossLoggingVersion}" + implementation(libs.org.jboss.logging.jboss.logging) // Testcontainers - implementation "org.testcontainers:postgresql:${testcontainersVersion}" + implementation(libs.org.testcontainers.postgresql) // Testing - testImplementation "org.assertj:assertj-core:${assertjVersion}" - testImplementation "io.vertx:vertx-junit5:${vertxSqlClientVersion}" + testImplementation(libs.org.assertj.assertj.core) + testImplementation(libs.io.vertx.vertx.junit5) } // Configuration for the tests diff --git a/integration-tests/verticle-postgres-it/build.gradle b/integration-tests/verticle-postgres-it/build.gradle index 4b48e87aa..2cd6edfec 100644 --- a/integration-tests/verticle-postgres-it/build.gradle +++ b/integration-tests/verticle-postgres-it/build.gradle @@ -11,37 +11,25 @@ buildscript { description = 'Bytecode enhancements integration tests' -ext { - jacksonDatabindVersion = '2.15.2' - jbossLoggingVersion = '3.5.0.Final' - assertjVersion = '3.26.3' - vertxWebVersion = project.hasProperty( 'vertxWebVersion' ) - ? project.property( 'vertxWebVersion' ) - : vertxSqlClientVersion - vertxWebClientVersion = project.hasProperty( 'vertxWebClientVersion' ) - ? project.property( 'vertxWebClientVersion' ) - : vertxSqlClientVersion -} - dependencies { implementation project(':hibernate-reactive-core') - implementation "io.vertx:vertx-web:${vertxWebVersion}" - implementation "io.vertx:vertx-web-client:${vertxWebClientVersion}" + implementation(libs.io.vertx.vertx.web) + implementation(libs.io.vertx.vertx.web.client) - runtimeOnly "io.vertx:vertx-pg-client:${vertxSqlClientVersion}" + runtimeOnly(libs.io.vertx.vertx.pg.client) // The Pg client requires this dependency - runtimeOnly "com.ongres.scram:client:2.1" - runtimeOnly "com.fasterxml.jackson.core:jackson-databind:${jacksonDatabindVersion}" + runtimeOnly(libs.com.ongres.scram.client) + runtimeOnly(libs.com.fasterxml.jackson.core.jackson.databind) // logging - implementation "org.jboss.logging:jboss-logging:${jbossLoggingVersion}" + implementation(libs.org.jboss.logging.jboss.logging) // Testcontainers - implementation "org.testcontainers:postgresql:${testcontainersVersion}" + implementation(libs.org.testcontainers.postgresql) // Testing - testImplementation "org.assertj:assertj-core:${assertjVersion}" - testImplementation "io.vertx:vertx-junit5:${vertxSqlClientVersion}" + testImplementation(libs.org.assertj.assertj.core) + testImplementation(libs.io.vertx.vertx.junit5) } // Configuration for the tests diff --git a/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/DockerImage.java b/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/DockerImage.java new file mode 100644 index 000000000..10026d020 --- /dev/null +++ b/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/DockerImage.java @@ -0,0 +1,125 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.it.verticle; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.testcontainers.utility.DockerImageName; + +/** + * A utility class with methods to generate a {@link DockerImageName} for Testcontainers. + *

+ * Testcontainers might not work if the image required is available in multiple different registries (for example when + * using podman instead of docker). + * These methods make sure to pick a registry as default. + *

+ */ +public final class DockerImage { + + /** + * The absolute path of the project root that we have set in Gradle. + */ + private static final String PROJECT_ROOT = System.getProperty( "hibernate.reactive.project.root" ); + + /** + * The path to the directory containing all the Dockerfile files + */ + private static final Path DOCKERFILE_DIR_PATH = Path.of( PROJECT_ROOT ).resolve( "tooling" ).resolve( "docker" ); + + /** + * Extract the image name and version from the first FROM instruction in the Dockerfile. + * Note that everything else is ignored. + */ + public static DockerImageName fromDockerfile(String databaseName) { + try { + final ImageInformation imageInformation = readFromInstruction( databaseName.toLowerCase() ); + return imageName( imageInformation.getRegistry(), imageInformation.getImage(), imageInformation.getVersion() ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } + } + + public static DockerImageName imageName(String registry, String image, String version) { + return DockerImageName + .parse( registry + "/" + image + ":" + version ) + .asCompatibleSubstituteFor( image ); + } + + private static class ImageInformation { + private final String registry; + private final String image; + private final String version; + + public ImageInformation(String fullImageInfo) { + // FullImageInfo pattern: /: + // For example: "docker.io/cockroachdb/cockroach:v24.3.13" becomes registry = "docker.io", image = "cockroachdb/cockroach", version = "v24.3.13" + final int registryEndPos = fullImageInfo.indexOf( '/' ); + final int imageEndPos = fullImageInfo.lastIndexOf( ':' ); + this.registry = fullImageInfo.substring( 0, registryEndPos ); + this.image = fullImageInfo.substring( registryEndPos + 1, imageEndPos ); + this.version = fullImageInfo.substring( imageEndPos + 1 ); + } + + public String getRegistry() { + return registry; + } + + public String getImage() { + return image; + } + + public String getVersion() { + return version; + } + + @Override + public String toString() { + return registry + "/" + image + ":" + version; + } + } + + private static Path dockerFilePath(String database) { + // Get project root from system property set by Gradle, with fallback + return DOCKERFILE_DIR_PATH.resolve( database.toLowerCase() + ".Dockerfile" ); + } + + private static ImageInformation readFromInstruction(String database) throws IOException { + return readFromInstruction( dockerFilePath( database ) ); + } + + /** + * Read a Dockerfile and extract the first FROM instruction. + * + * @param dockerfilePath path to the Dockerfile + * @return the first FROM instruction found, or empty if none found + * @throws IOException if the file cannot be read + */ + private static ImageInformation readFromInstruction(Path dockerfilePath) throws IOException { + if ( !Files.exists( dockerfilePath ) ) { + throw new FileNotFoundException( "Dockerfile not found: " + dockerfilePath ); + } + + List lines = Files.readAllLines( dockerfilePath ); + for ( String line : lines ) { + // Skip comments and empty lines + String trimmedLine = line.trim(); + if ( trimmedLine.isEmpty() || trimmedLine.startsWith( "#" ) ) { + continue; + } + + if ( trimmedLine.startsWith( "FROM " ) ) { + return new ImageInformation( trimmedLine.substring( "FROM ".length() ) ); + } + } + + throw new IOException( " Missing FROM instruction in " + dockerfilePath ); + } +} diff --git a/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/VertxServer.java b/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/VertxServer.java index eab23a6ff..f11254c6d 100644 --- a/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/VertxServer.java +++ b/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/VertxServer.java @@ -21,6 +21,7 @@ import io.vertx.core.Vertx; import io.vertx.core.VertxOptions; import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; import static java.lang.invoke.MethodHandles.lookup; import static org.hibernate.reactive.logging.impl.LoggerFactory.make; @@ -36,7 +37,8 @@ public class VertxServer { // These properties are in DatabaseConfiguration in core public static final boolean USE_DOCKER = Boolean.getBoolean( "docker" ); - public static final String IMAGE_NAME = "postgres:16.3"; + public static final DockerImageName IMAGE_NAME = DockerImage.fromDockerfile( "postgresql" ); + public static final String USERNAME = "hreact"; public static final String PASSWORD = "hreact"; public static final String DB_NAME = "hreact"; diff --git a/local-build-plugins/src/main/java/org/hibernate/reactive/env/VersionsPlugin.java b/local-build-plugins/src/main/java/org/hibernate/reactive/env/VersionsPlugin.java index 36b5ae10a..c34a4b479 100644 --- a/local-build-plugins/src/main/java/org/hibernate/reactive/env/VersionsPlugin.java +++ b/local-build-plugins/src/main/java/org/hibernate/reactive/env/VersionsPlugin.java @@ -28,6 +28,7 @@ public class VersionsPlugin implements Plugin { public static final String SKIP_ORM_VERSION_PARSING = "skipOrmVersionParsing"; public static final String RELATIVE_FILE = "gradle/version.properties"; + public static final String RELATIVE_CATALOG = "gradle/libs.versions.toml"; @Override public void apply(Project project) { @@ -57,12 +58,13 @@ public void apply(Project project) { project.getLogger().lifecycle( "Development version: n/a" ); } - final String ormVersionString = determineOrmVersion( project ); + final VersionsTomlParser tomlParser = new VersionsTomlParser( RELATIVE_CATALOG ); + final String ormVersionString = determineOrmVersion( project, tomlParser ); final Object ormVersion = resolveOrmVersion( ormVersionString, project ); project.getLogger().lifecycle( "ORM version: {}", ormVersion ); project.getExtensions().add( ORM_VERSION, ormVersion ); - final Object ormPluginVersion = determineOrmPluginVersion( ormVersion, project ); + final Object ormPluginVersion = determineOrmPluginVersion( ormVersion, project, tomlParser ); project.getLogger().lifecycle( "ORM Gradle plugin version: {}", ormPluginVersion ); project.getExtensions().add( ORM_PLUGIN_VERSION, ormPluginVersion ); } @@ -123,10 +125,17 @@ private static void withInputStream(File file, Consumer action) { } } - private String determineOrmVersion(Project project) { + private String determineOrmVersion(Project project, VersionsTomlParser parser) { + // Check if it has been set in the project if ( project.hasProperty( ORM_VERSION ) ) { return (String) project.property( ORM_VERSION ); } + + // Check in the catalog + final String version = parser.read( ORM_VERSION ); + if ( version != null ) { + return version; + } throw new IllegalStateException( "Hibernate ORM version not specified on project" ); } @@ -138,10 +147,18 @@ private Object resolveOrmVersion(String stringForm, Project project) { return new ProjectVersion( stringForm ); } - private Object determineOrmPluginVersion(Object ormVersion, Project project) { + private Object determineOrmPluginVersion(Object ormVersion, Project project, VersionsTomlParser parser) { + // Check if it has been set in the project if ( project.hasProperty( ORM_PLUGIN_VERSION ) ) { return project.property( ORM_PLUGIN_VERSION ); } - return ormVersion; + + // Check in the catalog + final String version = parser.read( ORM_PLUGIN_VERSION ); + if ( version != null ) { + return version; + } + + throw new IllegalStateException( "Hibernate ORM Gradle plugin version not specified on project" ); } } diff --git a/local-build-plugins/src/main/java/org/hibernate/reactive/env/VersionsTomlParser.java b/local-build-plugins/src/main/java/org/hibernate/reactive/env/VersionsTomlParser.java new file mode 100644 index 000000000..b0adafd6d --- /dev/null +++ b/local-build-plugins/src/main/java/org/hibernate/reactive/env/VersionsTomlParser.java @@ -0,0 +1,68 @@ +package org.hibernate.reactive.env; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Read the versions section of the library catalog + */ +public class VersionsTomlParser { + + private final Map data = new HashMap<>(); + + public VersionsTomlParser(String filePath) { + parse( filePath ); + } + + private void parse(String filePath) { + try ( BufferedReader reader = new BufferedReader( new FileReader( filePath ) ) ) { + String line; + String currentSection = null; + while ( ( line = reader.readLine() ) != null ) { + line = line.trim(); + + // Skip comments and blank lines + if ( line.isEmpty() || line.startsWith( "#" ) ) { + continue; + } + + // Handle [section] + if ( line.startsWith( "[" ) && line.endsWith( "]" ) ) { + currentSection = line.substring( 1, line.length() - 1 ).trim(); + continue; + } + + if ( "versions".equalsIgnoreCase( currentSection ) ) { + // Handle key = value + int equalsIndex = line.indexOf( '=' ); + if ( equalsIndex == -1 ) { + continue; + } + + String key = line.substring( 0, equalsIndex ).trim(); + String value = line.substring( equalsIndex + 1 ).trim(); + + // Remove optional quotes around string values + if ( value.startsWith( "\"" ) && value.endsWith( "\"" ) ) { + value = value.substring( 1, value.length() - 1 ); + } + + data.put( key, value ); + } + } + } + catch (IOException e) { + throw new RuntimeException( e ); + } + } + + /** + * Read the value of the property in the versions section of a toml file + */ + public String read(String property) { + return data.get( property ); + } +} diff --git a/podman.md b/podman.md index dc81fb828..9b9a71af2 100644 --- a/podman.md +++ b/podman.md @@ -14,13 +14,13 @@ If you replace `podman` with `sudo docker`, they will also work with [Docker][do Example: ``` -podman run --rm --name HibernateTestingPGSQL postgres:14.0 +podman run --rm --name HibernateTestingPGSQL postgres:17.5 ``` becomes for Docker: ``` -sudo docker run --rm --name HibernateTestingPGSQL postgres:14.0 +sudo docker run --rm --name HibernateTestingPGSQL postgres:17.5 ``` --- @@ -39,7 +39,7 @@ required credentials and schema to run the tests: podman run --rm --name HibernateTestingPGSQL \ -e POSTGRES_USER=hreact -e POSTGRES_PASSWORD=hreact -e POSTGRES_DB=hreact \ -e POSTGRES_INITDB_ARGS="-A password" \ - -p 5432:5432 docker.io/postgres:16.3 + -p 5432:5432 docker.io/postgres:17.5 ``` When the database has started, you can run the tests on PostgreSQL with: @@ -67,7 +67,7 @@ and schema to run the tests: ``` podman run --rm --name HibernateTestingMariaDB \ -e MYSQL_ROOT_PASSWORD=hreact -e MYSQL_DATABASE=hreact -e MYSQL_USER=hreact -e MYSQL_PASSWORD=hreact \ - -p 3306:3306 docker.io/mariadb:11.4.2 + -p 3306:3306 docker.io/mariadb:11.7.2 ``` When the database has started, you can run the tests on MariaDB with: @@ -94,7 +94,7 @@ and schema to run the tests: ``` podman run --rm --name HibernateTestingMySQL \ -e MYSQL_ROOT_PASSWORD=hreact -e MYSQL_DATABASE=hreact -e MYSQL_USER=hreact -e MYSQL_PASSWORD=hreact \ - -p 3306:3306 docker.io/mysql:8.4.0 + -p 3306:3306 docker.io/mysql:9.2.0 ``` When the database has started, you can run the tests on MySQL with: @@ -121,7 +121,7 @@ configured to run the tests: ``` podman run --rm --name=HibernateTestingCockroachDB \ --hostname=roachrr1 -p 26257:26257 -p 8080:8080 \ - docker.io/cockroachdb/cockroach:v24.1.0 start-single-node --insecure + docker.io/cockroachdb/cockroach:v24.3.13 start-single-node --insecure ``` Some of tests needs temporary tables and because this is an experimental feature in @@ -158,7 +158,7 @@ and schema to run the tests: podman run --rm -e LICENSE=accept --privileged=true --group-add keep-groups \ --name HibernateTestingDB2 -e DBNAME=hreact -e DB2INSTANCE=hreact \ -e DB2INST1_PASSWORD=hreact -e PERSISTENT_HOME=false -e ARCHIVE_LOGS=false \ - -e AUTOCONFIG=false -p 50000:50000 docker.io/icr.io/db2_community/db2:12.1.0.0 + -e AUTOCONFIG=false -p 50000:50000 icr.io/db2_community/db2:12.1.2.0 ``` When the database has started, you can run the tests on Db2 with: diff --git a/publish.gradle b/publish.gradle index 3c48e1298..747415921 100644 --- a/publish.gradle +++ b/publish.gradle @@ -1,13 +1,13 @@ +apply plugin: 'java' apply plugin: 'maven-publish' -tasks.register( 'sourcesJar', Jar ) { - from sourceSets.main.allJava - archiveClassifier = 'sources' -} +// Java / publishing -tasks.register( 'javadocJar', Jar ) { - from javadoc - archiveClassifier = 'javadoc' +java { + // Configure the Java "software component" to include javadoc and sources jars in addition to the classes jar. + // Ultimately, this component is what makes up the publication for this project. + withJavadocJar() + withSourcesJar() } jar { @@ -21,7 +21,7 @@ jar { 'Implementation-Version': project.version, 'Implementation-Vendor': 'Hibernate.org', 'Implementation-Vendor-Id': 'org.hibernate', - 'Implementation-Url': 'http://hibernate.org/reactive', + 'Implementation-Url': 'https://hibernate.org/reactive', ) } } @@ -35,14 +35,9 @@ javadoc { publishing { publications { - logger.lifecycle "Publishing groupId: '" + project.group + "', version: '" + project.version + "'" - - publishedArtifacts(MavenPublication) { - groupId = project.group - version = project.version + register( "publishedArtifacts", MavenPublication) { from components.java - artifact sourcesJar - artifact javadocJar + pom { name = project.mavenPomName description = project.description @@ -55,7 +50,7 @@ publishing { license { name = 'Apache License Version 2.0' url = 'https://opensource.org/licenses/Apache-2.0' - comments = 'See discussion at http://hibernate.org/community/license/ for more details.' + comments = 'See discussion at https://hibernate.org/community/license/ for more details.' distribution = 'repo' } } @@ -79,4 +74,25 @@ publishing { } } } + repositories { + maven { + name = "staging" + url = rootProject.layout.buildDirectory.dir("staging-deploy${File.separator}maven") + } + maven { + name = 'snapshots' + url = "https://central.sonatype.com/repository/maven-snapshots/" + // So that Gradle uses the `ORG_GRADLE_PROJECT_snapshotsPassword` / `ORG_GRADLE_PROJECT_snapshotsUsername` + // env variables to read the username/password for the `snapshots` repository publishing: + credentials(PasswordCredentials) + } + } +} + +def releasePrepareTask = tasks.register("releasePrepare") { + description "Prepares all the artifacts and documentation for a JReleaser release." + group "Release" + + // Create all the published artifacts (i.e. jars) and move them to the staging directory (for JReleaser): + dependsOn publishAllPublicationsToStagingRepository } diff --git a/release/build.gradle b/release/build.gradle index 2b507a9a2..ff80aff49 100644 --- a/release/build.gradle +++ b/release/build.gradle @@ -1,28 +1,13 @@ import java.nio.charset.StandardCharsets -ext { - // Select which repository to use for publishing the documentation - // Example: - // ./gradlew uploadDocumentation \ - // -PdocPublishRepoUri="git@github.com:DavideD/hibernate.org.git" \ - // -PdocPublishBranch="staging" - if ( !project.hasProperty('docPublishRepoUri') ) { - docPublishRepoUri = 'git@github.com:hibernate/hibernate.org.git' - } - if ( !project.hasProperty('docPublishBranch') ) { - docPublishBranch = 'staging' - } -} - description = 'Release module' // (Optional) before uploading the documentation, you can check // the generated website under release/build/hibernate.org with: // ./gradlew gitPublishCopy -// To publish the documentation: -// 1. Add the relevant SSH key to your SSH agent. -// 2. Execute this: -// ./gradlew uploadDocumentation -PdocPublishBranch=production +// The generated documentation are copied to the +// rootProject.layout.buildDirectory.dir("staging-deploy/documentation") by the updateDocumentationTask +// while the publishing is delegated to the https://github.com/hibernate/hibernate-release-scripts // To tag a version and trigger a release on CI (which will include publishing to Bintray and publishing documentation): // ./gradlew ciRelease -PreleaseVersion=x.y.z.Final -PdevelopmentVersion=x.y.z-SNAPSHOT -PgitRemote=origin -PgitBranch=main @@ -30,64 +15,6 @@ description = 'Release module' // The folder containing the rendered documentation final Directory documentationDir = project(":documentation").layout.buildDirectory.get() -// Relative path on the static website where the documentation is located -final String docWebsiteRelativePath = "reactive/documentation/${projectVersion.family}" - -// The location of the docs when the website has been cloned -final Directory docWebsiteReactiveFolder = project.layout.buildDirectory.dir( "docs-website/${docWebsiteRelativePath}" ).get() - -def releaseChecksTask = tasks.register( "releaseChecks" ) { - description 'Checks and preparation for release' - group 'Release' - - doFirst { - logger.lifecycle("Checking that the working tree is clean...") - String uncommittedFiles = executeGitCommand('status', '--porcelain') - if (!uncommittedFiles.isEmpty()) { - throw new GradleException( - "Cannot release because there are uncommitted or untracked files in the working tree.\n" + - "Commit or stash your changes first.\n" + - "Uncommitted files:\n " + - uncommittedFiles - ) - } - - String gitBranchLocal = project.hasProperty( 'gitBranch' ) && !project.property( 'gitBranch' ).isEmpty() - ? project.property( 'gitBranch' ) - : executeGitCommand( 'branch', '--show-current' ).trim() - - String gitRemoteLocal - if ( project.hasProperty( 'gitRemote' ) && !project.property( 'gitRemote' ).isEmpty() ) { - gitRemoteLocal = project.property( 'gitRemote' ) - } - else { - final String remotes = executeGitCommand( 'remote', 'show' ).trim() - final List tokens = remotes.tokenize() - if ( tokens.size() != 1 ) { - throw new GradleException( "Could not determine `gitRemote` property for `releaseChecks` tasks." ) - } - gitRemoteLocal = tokens.get( 0 ) - } - - project.ext { - gitBranch = gitBranchLocal - gitRemote = gitRemoteLocal - } - - logger.lifecycle( "Switching to branch '${project.gitBranch}'..." ) - executeGitCommand( 'checkout', project.gitBranch ) - - logger.lifecycle( "Checking that all commits are pushed..." ) - String diffWithUpstream = executeGitCommand( 'diff', '@{u}' ) - if ( !diffWithUpstream.isEmpty() ) { - throw new GradleException( - "Cannot perform `ciRelease` tasks because there are un-pushed local commits .\n" + - "Push your commits first." - ) - } - } -} - /** * Assembles all documentation into the {buildDir}/documentation directory. */ @@ -98,281 +25,36 @@ def assembleDocumentationTask = tasks.register( 'assembleDocumentation' ) { } assemble.dependsOn assembleDocumentationTask -/** -* Clone the website -*/ -def removeDocsWebsiteTask = tasks.register( 'removeDocsWebsite', Delete ) { - delete project.layout.buildDirectory.dir( "docs-website" ) -} - -def cloneDocsWebsiteTask = tasks.register( 'cloneDocsWebsite', Exec ) { - dependsOn removeDocsWebsiteTask - // Assure that the buildDir exists. Otherwise this task will fail. - dependsOn compileJava - workingDir project.layout.buildDirectory - commandLine 'git', 'clone', docPublishRepoUri, '-b', docPublishBranch, '--sparse', '--depth', '1', 'docs-website' -} - -def sparseCheckoutDocumentationTask = tasks.register( 'sparseCheckoutDocumentation', Exec ) { - dependsOn cloneDocsWebsiteTask - workingDir project.layout.buildDirectory.dir( "docs-website" ) - commandLine 'git', 'sparse-checkout', 'set', docWebsiteRelativePath -} - -/** -* Update the docs on the cloned website -*/ -def changeToReleaseVersionTask = tasks.register( 'changeToReleaseVersion' ) { - description 'Updates `gradle/version.properties` file to the specified release-version' - group 'Release' - - dependsOn releaseChecksTask - - doFirst { - logger.lifecycle( "Updating version-file to release-version : `${project.releaseVersion}`" ) - updateVersionFile( "${project.releaseVersion}" ) - } -} - def updateDocumentationTask = tasks.register( 'updateDocumentation' ) { description "Update the documentation on the cloned static website" - dependsOn assembleDocumentationTask, sparseCheckoutDocumentationTask - mustRunAfter changeToReleaseVersion + dependsOn assembleDocumentationTask // copy documentation outputs into target/documentation: // * this is used in building the dist bundles // * it is also used as a base to build a staged directory for documentation upload doLast { - // delete the folders in case some files have been removed - delete docWebsiteReactiveFolder.dir("javadocs"), docWebsiteReactiveFolder.dir("reference") - // Aggregated JavaDoc - copy { - from documentationDir.dir("javadocs") - into docWebsiteReactiveFolder.dir("javadocs") - } + copy { + from documentationDir.dir("javadocs") + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/javadocs") + } // Reference Documentation - copy { - from documentationDir.dir("asciidoc/reference/html_single") - into docWebsiteReactiveFolder.dir("reference/html_single") - } - } -} - -def stageDocChangesTask = tasks.register( 'stageDocChanges', Exec ) { - dependsOn updateDocumentationTask - workingDir project.layout.buildDirectory.dir( "docs-website" ) - commandLine 'git', 'add', '-A', '.' -} - -def commitDocChangesTask = tasks.register( 'commitDocChanges', Exec ) { - dependsOn stageDocChangesTask - workingDir project.layout.buildDirectory.dir( "docs-website" ) - commandLine 'git', 'commit', '-m', "[HR] Hibernate Reactive documentation for ${projectVersion}" -} - -def pushDocChangesTask = tasks.register( 'pushDocChanges', Exec ) { - description "Push documentation changes on the remote repository" - dependsOn commitDocChangesTask - workingDir project.layout.buildDirectory.dir( "docs-website" ) - commandLine 'git', 'push', '--atomic', 'origin', docPublishBranch -} - -def uploadDocumentationTask = tasks.register( 'uploadDocumentation' ) { - description "Upload documentation on the website" - group "Release" - dependsOn pushDocChangesTask - - doLast { - logger.lifecycle "Documentation published on '${docPublishRepoUri}' branch '${docPublishBranch}'" - } -} - -def gitPreparationForReleaseTask = tasks.register( 'gitPreparationForRelease' ) { - dependsOn releaseChecksTask, changeToReleaseVersionTask - finalizedBy updateDocumentationTask - - doLast { - logger.lifecycle( "Performing pre-steps Git commit : `${project.releaseVersion}`" ) - executeGitCommand( 'add', '.' ) - executeGitCommand( 'commit', '-m', "Update project version to : `${project.releaseVersion}`" ) - } -} - -def changeToDevelopmentVersionTask = tasks.register( 'changeToDevelopmentVersion' ) { - description 'Updates `gradle/version.properties` file to the specified development-version' - group 'Release' - - dependsOn releaseChecksTask - - doFirst { - logger.lifecycle( "Updating version-file to development-version : `${project.developmentVersion}`" ) - updateVersionFile( "${project.developmentVersion}" ) - } -} - -def releasePreparePostGitTask = tasks.register( 'gitTasksAfterRelease' ) { - dependsOn changeToDevelopmentVersionTask - - doLast { - if ( project.createTag ) { - logger.lifecycle( "Tagging release : `${project.releaseTag}`..." ) - executeGitCommand( 'tag', '-a', project.releaseTag, '-m', "Release $project.projectVersion" ) - } - - logger.lifecycle( "Performing post-steps Git commit : `${project.releaseVersion}`" ) - executeGitCommand( 'add', '.' ) - executeGitCommand( 'commit', '-m', "Update project version to : `${project.developmentVersion}`" ) - } -} - -void updateVersionFile(var version) { - logger.lifecycle( "Updating `gradle/version.properties` version to `${version}`" ) - project.versionFile.text = "projectVersion=${version}" - project.version = version -} - -def publishReleaseArtifactsTask = tasks.register( 'publishReleaseArtifacts' ) { - dependsOn uploadDocumentationTask - dependsOn ":hibernate-reactive-core:publishToSonatype" - - mustRunAfter gitPreparationForReleaseTask -} - -def releasePerformPostGitTask = tasks.register( 'gitTasksAfterReleasePerform' ) { - - doLast { - if ( project.createTag ) { - logger.lifecycle( "Pushing branch and tag to remote `${project.gitRemote}`..." ) - executeGitCommand( 'push', '--atomic', project.gitRemote, project.gitBranch, project.releaseTag ) - } - else { - logger.lifecycle( "Pushing branch to remote `${project.gitRemote}`..." ) - executeGitCommand( 'push', project.gitRemote, project.gitBranch ) - } + copy { + from documentationDir.dir("asciidoc/reference/html_single") + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/reference/html_single") + } } } def releasePrepareTask = tasks.register( "releasePrepare" ) { - description "On a local checkout, performs all the changes required for the release, website updates included" + description "Prepares all the artifacts and documentation for a JReleaser release." group "Release" - dependsOn gitPreparationForReleaseTask - - finalizedBy releasePreparePostGitTask -} - -def releasePerformTask = tasks.register( 'releasePerform' ) { - group 'Release' - description 'Performs a release on local check-out, including updating changelog and ' - - dependsOn publishReleaseArtifactsTask - - finalizedBy releasePerformPostGitTask -} - -/* -* Release everything -*/ -def releaseTask = tasks.register( 'release' ) { - description 'Performs a release on local check-out' - group 'Release' - - dependsOn releasePrepareTask - dependsOn releasePerformTask -} - -def ciReleaseTask = tasks.register( 'ciRelease' ) { - description "Triggers the release on CI: creates commits to change the version (release, then development), creates a tag, pushes everything. Then CI will take over and perform the release." - group "Release" - - dependsOn releaseTask -} - -static String executeGitCommand(Object ... subcommand){ - List command = ['git'] - Collections.addAll( command, subcommand ) - def proc = command.execute() - def code = proc.waitFor() - def stdout = inputStreamToString( proc.getInputStream() ) - def stderr = inputStreamToString( proc.getErrorStream() ) - if ( code != 0 ) { - throw new GradleException( "An error occurred while executing " + command + "\n\nstdout:\n" + stdout + "\n\nstderr:\n" + stderr ) - } - return stdout -} - -static String inputStreamToString(InputStream inputStream) { - inputStream.withCloseable { ins -> - new BufferedInputStream(ins).withCloseable { bis -> - new ByteArrayOutputStream().withCloseable { buf -> - int result = bis.read() - while (result != -1) { - buf.write((byte) result) - result = bis.read() - } - return buf.toString(StandardCharsets.UTF_8.name()) - } - } - } -} - -gradle.getTaskGraph().whenReady { tg-> - if ( ( tg.hasTask( project.tasks.releasePrepare ) || tg.hasTask( project.tasks.releasePerform ) ) - && ! project.getGradle().getStartParameter().isDryRun() ) { - String releaseVersionLocal - String developmentVersionLocal - - def console = tg.hasTask( project.tasks.release ) && !tg.hasTask( project.tasks.ciRelease ) - ? System.console() - : null - - if (project.hasProperty('releaseVersion')) { - releaseVersionLocal = project.property('releaseVersion') - } - else { - if (console) { - // prompt for `releaseVersion` - releaseVersionLocal = console.readLine('> Enter the release version: ') - } - else { - throw new GradleException( - "`release`-related tasks require the following properties: 'releaseVersion', 'developmentVersion'" - ) - } - } - - if (project.hasProperty('developmentVersion')) { - developmentVersionLocal = project.property('developmentVersion') - } - else { - if (console) { - // prompt for `developmentVersion` - developmentVersionLocal = console.readLine('> Enter the next development version: ') - } - else { - throw new GradleException( - "`release`-related tasks require the following properties: 'releaseVersion', 'developmentVersion'" - ) - } - } - - assert releaseVersionLocal != null && developmentVersionLocal != null - - // set up information for the release-related tasks - project.ext { - releaseVersion = releaseVersionLocal - developmentVersion = developmentVersionLocal - createTag = !project.hasProperty('noTag') - releaseTag = project.createTag ? determineReleaseTag(releaseVersionLocal) : '' - } - } -} - -static String determineReleaseTag(String releaseVersion) { - return releaseVersion.endsWith( '.Final' ) - ? releaseVersion.replace( ".Final", "" ) - : releaseVersion + // Render the Documentation/Javadocs and move them to the staging directory (for JReleaser): + dependsOn updateDocumentationTask + // Create all the published artifacts (i.e. jars) and move them to the staging directory (for JReleaser): + // this one is defined in the publish.gradle + // dependsOn project.getTasksByName(publishAllPublicationsToStagingRepository, false) } diff --git a/settings.gradle b/settings.gradle index af4c707eb..b894dfc9a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,6 +19,44 @@ gradle.ext.baselineJavaVersion = JavaLanguageVersion.of( 11 ) // You can't use bytecode higher than what Gradle supports, even with toolchains. def GRADLE_MAX_SUPPORTED_BYTECODE_VERSION = 23 +// This overrides the default version catalog in gradle/libs.versions.toml, which can be +// useful to monitor compatibility for upcoming versions on CI: +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + // ./gradlew build -PhibernateOrmVersion=7.0.2.Final + def hibernateOrmVersion = settings.ext.find("hibernateOrmVersion") ?: "" + if ( hibernateOrmVersion != "" ) { + version("hibernateOrmVersion", hibernateOrmVersion) + } + + // ./gradlew build -PhibernateOrmGradlePluginVersion=7.0.2.Final + def hibernateOrmGradlePluginVersion = settings.ext.find("hibernateOrmGradlePluginVersion") ?: "" + if ( hibernateOrmGradlePluginVersion ) { + version("hibernateOrmGradlePluginVersion", hibernateOrmGradlePluginVersion) + } + + // ./gradlew build -PvertxSqlClientVersion=4.0.0-SNAPSHOT + def vertxSqlClientVersion = settings.ext.find("vertxSqlClientVersion") ?: "" + if ( vertxSqlClientVersion ) { + version("vertxSqlClientVersion", vertxSqlClientVersion) + } + + // ./gradlew build -PvertxWebVersion=4.0.0-SNAPSHOT + def vertxWebVersion = settings.ext.find("vertxWebVersion") ?: vertxSqlClientVersion + if ( vertxWebVersion ) { + version("vertxWebVersion", vertxWebVersion) + } + + // ./gradlew build -PvertxWebClientVersion=4.0.0-SNAPSHOT + def vertxWebClientVersion = settings.ext.find("vertxWebClientVersion") ?: vertxSqlClientVersion + if ( vertxWebClientVersion ) { + version("vertxWebClientVersion", vertxWebClientVersion) + } + } + } +} + // If either 'main.jdk.version' or 'test.jdk.version' is set, enable the toolchain and use the selected jdk. // If only one property is set, the other defaults to the baseline Java version (8). // Note that when toolchain is enabled, you also need to specify diff --git a/tooling/docker/README.md b/tooling/docker/README.md new file mode 100644 index 000000000..4de2451e0 --- /dev/null +++ b/tooling/docker/README.md @@ -0,0 +1,6 @@ +Our test suite will only read the first FROM instruction from each Dockerfile to extract the base image and the version +of the container to run. It will ignore everything else. + +The reason we have these files is that we want to automate the upgrade of the containers using dependabot. + +See the class `DockerImage`. diff --git a/tooling/docker/cockroachdb.Dockerfile b/tooling/docker/cockroachdb.Dockerfile new file mode 100644 index 000000000..0b441f7ca --- /dev/null +++ b/tooling/docker/cockroachdb.Dockerfile @@ -0,0 +1,3 @@ +# CockroachDB +# See https://hub.docker.com/r/cockroachdb/cockroach +FROM docker.io/cockroachdb/cockroach:v25.3.4 diff --git a/tooling/docker/db2.Dockerfile b/tooling/docker/db2.Dockerfile new file mode 100644 index 000000000..fbd438115 --- /dev/null +++ b/tooling/docker/db2.Dockerfile @@ -0,0 +1,3 @@ +# IBM DB2 +# See https://hub.docker.com/r/ibmcom/db2 +FROM icr.io/db2_community/db2:12.1.3.0 diff --git a/tooling/docker/maria.Dockerfile b/tooling/docker/maria.Dockerfile new file mode 100644 index 000000000..6aa9bf898 --- /dev/null +++ b/tooling/docker/maria.Dockerfile @@ -0,0 +1,3 @@ +# MariaDB +# See https://hub.docker.com/_/mariadb +FROM docker.io/mariadb:12.0.2 diff --git a/tooling/docker/mysql.Dockerfile b/tooling/docker/mysql.Dockerfile new file mode 100644 index 000000000..03918b8e3 --- /dev/null +++ b/tooling/docker/mysql.Dockerfile @@ -0,0 +1,3 @@ +# MySQL +# See https://hub.docker.com/_/mysql +FROM container-registry.oracle.com/mysql/community-server:9.5.0 diff --git a/tooling/docker/oracle.Dockerfile b/tooling/docker/oracle.Dockerfile new file mode 100644 index 000000000..7dfc6447d --- /dev/null +++ b/tooling/docker/oracle.Dockerfile @@ -0,0 +1,3 @@ +# Oracle Database Free +# See https://hub.docker.com/r/gvenzl/oracle-free +FROM docker.io/gvenzl/oracle-free:23-slim-faststart diff --git a/tooling/docker/postgresql.Dockerfile b/tooling/docker/postgresql.Dockerfile new file mode 100644 index 000000000..f2daff3c7 --- /dev/null +++ b/tooling/docker/postgresql.Dockerfile @@ -0,0 +1,3 @@ +# PostgreSQL +# See https://hub.docker.com/_/postgres +FROM docker.io/postgres:18.0 diff --git a/tooling/docker/sqlserver.Dockerfile b/tooling/docker/sqlserver.Dockerfile new file mode 100644 index 000000000..94fae1429 --- /dev/null +++ b/tooling/docker/sqlserver.Dockerfile @@ -0,0 +1,3 @@ +# Microsoft SQL Server +# See https://hub.docker.com/_/microsoft-mssql-server +FROM mcr.microsoft.com/mssql/server:2025-latest diff --git a/tooling/jbang/CockroachDBReactiveTest.java.qute b/tooling/jbang/CockroachDBReactiveTest.java.qute index 17eecb479..8b87070b8 100755 --- a/tooling/jbang/CockroachDBReactiveTest.java.qute +++ b/tooling/jbang/CockroachDBReactiveTest.java.qute @@ -5,12 +5,12 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-pg-client:$\{vertx.version:4.5.11} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.11} +//DEPS io.vertx:vertx-pg-client:$\{vertx.version:4.5.16} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.16} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:2.4.0.Final} //DEPS org.assertj:assertj-core:3.26.3 //DEPS junit:junit:4.13.2 -//DEPS org.testcontainers:cockroachdb:1.20.4 +//DEPS org.testcontainers:cockroachdb:1.21.3 //DEPS org.slf4j:slf4j-simple:2.0.7 //// Testcontainer needs the JDBC drivers to start the container @@ -70,7 +70,7 @@ public class {baseName} { } @ClassRule - public final static CockroachContainer database = new CockroachContainer( imageName( "cockroachdb", "cockroach", "v24.1.0" ) ); + public final static CockroachContainer database = new CockroachContainer( imageName( "cockroachdb", "cockroach", "v24.3.13" ) ); private Mutiny.SessionFactory sessionFactory; diff --git a/tooling/jbang/Db2ReactiveTest.java.qute b/tooling/jbang/Db2ReactiveTest.java.qute index 2e6ae3623..8e8c6554c 100755 --- a/tooling/jbang/Db2ReactiveTest.java.qute +++ b/tooling/jbang/Db2ReactiveTest.java.qute @@ -5,12 +5,12 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-db2-client:$\{vertx.version:4.5.11} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.11} +//DEPS io.vertx:vertx-db2-client:$\{vertx.version:4.5.16} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.16} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:2.4.0.Final} //DEPS org.assertj:assertj-core:3.26.3 //DEPS junit:junit:4.13.2 -//DEPS org.testcontainers:db2:1.20.4 +//DEPS org.testcontainers:db2:1.21.3 //DEPS org.slf4j:slf4j-simple:2.0.7 import jakarta.persistence.Entity; diff --git a/tooling/jbang/Example.java b/tooling/jbang/Example.java index fe0b312e8..8e8ec3091 100644 --- a/tooling/jbang/Example.java +++ b/tooling/jbang/Example.java @@ -6,9 +6,9 @@ */ //DEPS com.ongres.scram:client:2.1 -//DEPS io.vertx:vertx-pg-client:${vertx.version:4.5.11} -//DEPS io.vertx:vertx-mysql-client:${vertx.version:4.5.11} -//DEPS io.vertx:vertx-db2-client:${vertx.version:4.5.11} +//DEPS io.vertx:vertx-pg-client:${vertx.version:4.5.16} +//DEPS io.vertx:vertx-mysql-client:${vertx.version:4.5.16} +//DEPS io.vertx:vertx-db2-client:${vertx.version:4.5.16} //DEPS org.hibernate.reactive:hibernate-reactive-core:${hibernate-reactive.version:2.4.0.Final} //DEPS org.slf4j:slf4j-simple:2.0.7 //DESCRIPTION Allow authentication to PostgreSQL using SCRAM: @@ -59,7 +59,7 @@ *
  *                 podman run --rm --name HibernateTestingPGSQL \
  *                      -e POSTGRES_USER=hreact -e POSTGRES_PASSWORD=hreact -e POSTGRES_DB=hreact \
- *                      -p 5432:5432 postgres:16.3
+ *                      -p 5432:5432 postgres:17.5
  *              
* *
3. Run the example with JBang
diff --git a/tooling/jbang/MariaDBReactiveTest.java.qute b/tooling/jbang/MariaDBReactiveTest.java.qute index b7592b05b..444e0421d 100755 --- a/tooling/jbang/MariaDBReactiveTest.java.qute +++ b/tooling/jbang/MariaDBReactiveTest.java.qute @@ -5,12 +5,12 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-mysql-client:$\{vertx.version:4.5.11} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.11} +//DEPS io.vertx:vertx-mysql-client:$\{vertx.version:4.5.16} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.16} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:2.4.0.Final} //DEPS org.assertj:assertj-core:3.26.3 //DEPS junit:junit:4.13.2 -//DEPS org.testcontainers:mariadb:1.20.4 +//DEPS org.testcontainers:mariadb:1.21.3 //DEPS org.slf4j:slf4j-simple:2.0.7 //// Testcontainer needs the JDBC drivers to start the container @@ -69,7 +69,7 @@ public class {baseName} { } @ClassRule - public final static MariaDBContainer database = new MariaDBContainer<>( imageName( "docker.io", "mariadb", "11.4.2" ) ); + public final static MariaDBContainer database = new MariaDBContainer<>( imageName( "docker.io", "mariadb", "11.7.2" ) ); private Mutiny.SessionFactory sessionFactory; diff --git a/tooling/jbang/MySQLReactiveTest.java.qute b/tooling/jbang/MySQLReactiveTest.java.qute index 7cc856c4a..099184c04 100755 --- a/tooling/jbang/MySQLReactiveTest.java.qute +++ b/tooling/jbang/MySQLReactiveTest.java.qute @@ -5,12 +5,12 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-mysql-client:$\{vertx.version:4.5.11} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.11} +//DEPS io.vertx:vertx-mysql-client:$\{vertx.version:4.5.16} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.16} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:2.4.0.Final} //DEPS org.assertj:assertj-core:3.26.3 //DEPS junit:junit:4.13.2 -//DEPS org.testcontainers:mysql:1.20.4 +//DEPS org.testcontainers:mysql:1.21.3 //DEPS org.slf4j:slf4j-simple:2.0.7 //// Testcontainer needs the JDBC drivers to start the container @@ -72,7 +72,7 @@ public class {baseName} { } @ClassRule - public final static MySQLContainer database = new MySQLContainer<>( imageName( "docker.io", "mysql", "8.4.0" ) ); + public final static MySQLContainer database = new MySQLContainer<>( imageName( "container-registry.oracle.com", "mysql/community-server", "9.3.0" ).asCompatibleSubstituteFor( "mysql" ) ); private Mutiny.SessionFactory sessionFactory; diff --git a/tooling/jbang/PostgreSQLReactiveTest.java.qute b/tooling/jbang/PostgreSQLReactiveTest.java.qute index 306c30476..2c32c08c9 100755 --- a/tooling/jbang/PostgreSQLReactiveTest.java.qute +++ b/tooling/jbang/PostgreSQLReactiveTest.java.qute @@ -5,12 +5,12 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-pg-client:$\{vertx.version:4.5.11} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.11} +//DEPS io.vertx:vertx-pg-client:$\{vertx.version:4.5.16} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.16} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:2.4.0.Final} //DEPS org.assertj:assertj-core:3.26.3 //DEPS junit:junit:4.13.2 -//DEPS org.testcontainers:postgresql:1.20.4 +//DEPS org.testcontainers:postgresql:1.21.3 //DEPS org.slf4j:slf4j-simple:2.0.7 //DESCRIPTION Allow authentication to PostgreSQL using SCRAM: //DEPS com.ongres.scram:client:2.1 @@ -67,7 +67,7 @@ public class {baseName} { } @ClassRule - public final static PostgreSQLContainer database = new PostgreSQLContainer( imageName( "docker.io", "postgres", "16.3" ) ); + public final static PostgreSQLContainer database = new PostgreSQLContainer( imageName( "docker.io", "postgres", "17.5" ) ); private Mutiny.SessionFactory sessionFactory; diff --git a/tooling/jbang/ReactiveTest.java b/tooling/jbang/ReactiveTest.java index c79d14fe5..54e77b3ef 100755 --- a/tooling/jbang/ReactiveTest.java +++ b/tooling/jbang/ReactiveTest.java @@ -5,19 +5,19 @@ */ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS io.vertx:vertx-pg-client:${vertx.version:4.5.11} +//DEPS io.vertx:vertx-pg-client:${vertx.version:4.5.16} //DEPS com.ongres.scram:client:2.1 -//DEPS io.vertx:vertx-db2-client:${vertx.version:4.5.11} -//DEPS io.vertx:vertx-mysql-client:${vertx.version:4.5.11} -//DEPS io.vertx:vertx-unit:${vertx.version:4.5.11} +//DEPS io.vertx:vertx-db2-client:${vertx.version:4.5.16} +//DEPS io.vertx:vertx-mysql-client:${vertx.version:4.5.16} +//DEPS io.vertx:vertx-unit:${vertx.version:4.5.16} //DEPS org.hibernate.reactive:hibernate-reactive-core:${hibernate-reactive.version:2.4.0.Final} //DEPS org.assertj:assertj-core:3.26.3 //DEPS junit:junit:4.13.2 -//DEPS org.testcontainers:postgresql:1.20.4 -//DEPS org.testcontainers:mysql:1.20.4 -//DEPS org.testcontainers:db2:1.20.4 -//DEPS org.testcontainers:mariadb:1.20.4 -//DEPS org.testcontainers:cockroachdb:1.20.4 +//DEPS org.testcontainers:postgresql:1.21.3 +//DEPS org.testcontainers:mysql:1.21.3 +//DEPS org.testcontainers:db2:1.21.3 +//DEPS org.testcontainers:mariadb:1.21.3 +//DEPS org.testcontainers:cockroachdb:1.21.3 // //// Testcontainer needs the JDBC drivers to start the containers //// Hibernate Reactive doesn't use them @@ -228,11 +228,11 @@ public String toString() { * It's a wrapper around the testcontainers classes. */ enum Database { - POSTGRESQL( () -> new PostgreSQLContainer( "postgres:16.3" ) ), - MYSQL( () -> new MySQLContainer( "mysql:8.4.0" ) ), + POSTGRESQL( () -> new PostgreSQLContainer( "postgres:17.5" ) ), + MYSQL( () -> new MySQLContainer( "container-registry.oracle.com/mysql/community-server:9.3.0" ) ), DB2( () -> new Db2Container( "docker.io/icr.io/db2_community/db2:12.1.0.0" ).acceptLicense() ), - MARIADB( () -> new MariaDBContainer( "mariadb:11.4.2" ) ), - COCKROACHDB( () -> new CockroachContainer( "cockroachdb/cockroach:v24.1.0" ) ); + MARIADB( () -> new MariaDBContainer( "mariadb:11.7.2" ) ), + COCKROACHDB( () -> new CockroachContainer( "cockroachdb/cockroach:v24.3.13" ) ); private final Supplier> containerSupplier;