diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 442c46f96a72..fdb4c95ed1ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,15 +1,9 @@ name: GH Actions CI on: - push: - branches: - # Pattern order matters: the last matching inclusion/exclusion wins - - 'main' - # We don't want to run CI on branches for dependabot, just on the PR. - - '!dependabot/**' pull_request: branches: - - 'main' + - '7.1' # Ignore dependabot PRs that are not just about build dependencies or workflows; # we'll reject such PRs and send one ourselves. - '!dependabot/**' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 89ed21898618..46596363859c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,11 +1,9 @@ name: "CodeQL" on: - push: - branches: [ 'main' ] pull_request: # The branches below must be a subset of the branches above - branches: [ 'main' ] + branches: [ '7.1' ] schedule: - cron: '34 11 * * 4' diff --git a/Jenkinsfile b/Jenkinsfile index e4d60d35846e..7334b9018f0b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -40,13 +40,13 @@ stage('Configure') { // Don't build with HANA by default, but only do it nightly until we receive a 3rd instance // new BuildEnvironment( dbName: 'hana_cloud', dbLockableResource: 'hana-cloud', dbLockResourceAsHost: true ), new BuildEnvironment( node: 's390x' ), + new BuildEnvironment( dbName: 'sybase_jconn' ), // We generally build with JDK 21, but our baseline is Java 17, so we test with JDK 17, to be sure everything works. // Here we even compile the main code with JDK 17, to be sure no JDK 18+ classes are depended on. new BuildEnvironment( mainJdkVersion: '17', testJdkVersion: '17' ), // We want to enable preview features when testing newer builds of OpenJDK: // even if we don't use these features, just enabling them can cause side effects // and it's useful to test that. - new BuildEnvironment( testJdkVersion: '24', testJdkLauncherArgs: '--enable-preview', additionalOptions: '-PskipJacoco=true' ), new BuildEnvironment( testJdkVersion: '25', testJdkLauncherArgs: '--enable-preview', additionalOptions: '-PskipJacoco=true' ), // The following JDKs aren't supported by Hibernate ORM out-of-the box yet: // they require the use of -Dnet.bytebuddy.experimental=true. @@ -97,17 +97,17 @@ if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { currentBuild.result = 'NOT_BUILT' return } +// This is a limited maintenance branch, so don't run this on pushes to the branch, only on PRs +if ( !env.CHANGE_ID ) { + print "INFO: Build skipped because this job should only run for pull request, not for branch pushes" + currentBuild.result = 'NOT_BUILT' + return +} stage('Build') { Map executions = [:] Map> state = [:] environments.each { BuildEnvironment buildEnv -> - // Don't build environments for newer JDKs when this is a PR, unless the PR is labelled with 'jdk' or 'jdk-' - if ( helper.scmSource.pullRequest && - buildEnv.testJdkVersion && buildEnv.testJdkVersion.toInteger() > DEFAULT_JDK_VERSION.toInteger() && - !pullRequest.labels.contains( 'jdk' ) && !pullRequest.labels.contains( "jdk-${buildEnv.testJdkVersion}" ) ) { - return - } state[buildEnv.tag] = [:] executions.put(buildEnv.tag, { runBuildOnNode(buildEnv.node ?: NODE_PATTERN_BASE) { @@ -208,6 +208,9 @@ stage('Build') { } }) } + executions.put('Hibernate Search Update Dependency', { + build job: '/hibernate-search-dependency-update/8.1', propagate: true, parameters: [string(name: 'UPDATE_JOB', value: 'orm7'), string(name: 'ORM_REPOSITORY', value: helper.scmSource.remoteUrl), string(name: 'ORM_PULL_REQUEST_ID', value: helper.scmSource.pullRequest.id)] + }) parallel(executions) } diff --git a/changelog.txt b/changelog.txt index 2b71cbffde3d..19735056e103 100644 --- a/changelog.txt +++ b/changelog.txt @@ -3,6 +3,101 @@ Hibernate 7 Changelog Note: Please refer to JIRA to learn more about each issue. +Changes in 7.1.4.Final (October 12, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/35519 + +** Bug + * [HHH-19851] - Session#findMultiple(EntityGraph...) fails for dynamic entities + * [HHH-19848] - NPE when using MySQLLegacyDialect + * [HHH-19840] - JDBC batching is not working with @CreationTimestamp and @UpdateTimestamp from Hibernate 7 + * [HHH-19824] - DB2zDialect does not use correct querySequenceString on a DB2 on zOs + * [HHH-19759] - joining a map key of basic type + * [HHH-19723] - Hibernate-testing depends on outdated Jakarta libraries, leading to compilation issues for Jakarta Data repositories + * [HHH-19630] - Hibernate Processor may fail if the return type is a single entity and annotated with multiple annotations + * [HHH-19629] - Hibernate Processor may fail when repository method parameter has more than one annotation (e.g. multiple constraints) + * [HHH-19393] - Hibernate Envers can not handle ID class if it is implemented as Java record + * [HHH-19085] - NPE when using null value in CriteriaUpdate + +** Improvement + * [HHH-19767] - Include license file in the META-INF of published artifacts + + +Changes in 7.1.3.Final (October 05, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/35421 + +** Improvement + * [HHH-19825] - Add methods and change method access for Hibernate Reactive + * [HHH-19717] - CockroachDB supports insert and update returning clause + +** Task + * [HHH-19800] - Migrate to release scripts for documentation publishing + +** Bug + * [HHH-18885] - ClassCastException with queued persist in an extra lazy Map + * [HHH-19781] - Subsequent uses of Criteria SelectionSpecification lead to duplicated specifications + + +Changes in 7.1.2.Final (September 29, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/35322 + +** Bug + * [HHH-19792] - Wrong query generated when an EmbeddedId is used in a WHERE clause using the IN operator and NativeParameterMarkerStrategy + * [HHH-19784] - Bytecode enhancement generates wrong field access method for classes in different JARs but with same package name deployed in the same EAR + * [HHH-19768] - Wrong supportsRowValueConstructorSyntaxInInSubQuery leads to bad performing queries + * [HHH-19713] - Hibernate creates invalid SQLServer query when using pagination with Jakarta Data + * [HHH-19681] - AssertionError on extract results from array containing jsonb values + + +Changes in 7.1.1.Final (September 14, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/34792 + +** Bug + * [HHH-19756] - Invalid SQL generated when using treat() with joined = discriminator inheritance and same attribute names + * [HHH-19753] - SelfRenderingSqmFunction equals/hashCode implementation not consistent with literals + * [HHH-19750] - org.hibernate.sql.exec.ExecutionException when running CriteriaUpdate with specific attribute/parameter configuration + * [HHH-19747] - Hibernate Envers can not handle @EnumeratedValue annotation + * [HHH-19745] - Equals implementation for SqmPath affects previous identity sensitive checks + * [HHH-19740] - Collection table deletion for table per class subclass entity fails with UnknownTableReferenceException + * [HHH-19738] - JDBC password logged when specified via jakarta.persistence.jdbc.password + * [HHH-19734] - Cache hit of bytecode enhanced proxy with shallow query cache layout fails + * [HHH-19732] - @OnDelete on owning collection reset when mapping bidirectional + * [HHH-19729] - Column check constraint not correctly hoisted to table level + * [HHH-19721] - Jakarta Data is missing from hibernate-platform (BOM) + * [HHH-19719] - org.hibernate.query.sqm.function.SelfRenderingSqmWindowFunction#appendHqlString throws IndexOutOfBoundsException when has no arguments + * [HHH-19716] - Collection event listeners may be missing collection owners in the persistent collection (PersistentCollection#getOwner==null) + * [HHH-19712] - Column deduplication leads to wrong alias calculation for native query alias expansion + * [HHH-19707] - Include value column name in TableStructure InitCommand + * [HHH-19703] - NativeQueryImpl#addScalar(String, Class) fails when no JavaType is registered + * [HHH-19699] - String JSON document writer code only runs with assertions + * [HHH-19695] - Invalid SQL generated for FETCH FIRST/NEXT clause + * [HHH-19688] - @IdClass is ignored for classes that have a single id field + * [HHH-19687] - Criteria query with lazy @OneToOne and @EmbeddedId throws exception + * [HHH-19648] - Recursive @Embeddable mapping leads to stack overflow + * [HHH-19605] - Session.isDirty might return true when batch fetching entity with CacheConcurrencyStrategy.READ_WRITE + * [HHH-19589] - @Converter does not take precedence over @TypeRegistration + * [HHH-19453] - sequence support not working on db2 As400 7.3 + * [HHH-19326] - Jakarta Data CrudRepository is generated without Types in UpdateAll, InsertAll and SaveAll Methods + * [HHH-19201] - BlobProxy with InputStream reads whole stream into a byte array + * [HHH-18686] - org.hibernate.tool.hbm2ddl.SchemaExport does not call orderColumns + * [HHH-17522] - Support correlation of CTEs + +** Improvement + * [HHH-19701] - MariaDB: "drop sequence" should add "if exists" + * [HHH-19697] - Use case-insensitive Dialect query pattern matching + * [HHH-19696] - Do not iterate over validated path in the TraversableResolver unless necessary + +** Task + * [HHH-19692] - Drop org.hibernate.boot.jaxb.spi.XmlSource + + Changes in 7.1.0.Final (August 08, 2025) ------------------------------------------------------------------------------------------------------------------------ diff --git a/ci/jpa-3.2-tck.Jenkinsfile b/ci/jpa-3.2-tck.Jenkinsfile index 111f58fb8967..786990c7c654 100644 --- a/ci/jpa-3.2-tck.Jenkinsfile +++ b/ci/jpa-3.2-tck.Jenkinsfile @@ -6,17 +6,11 @@ if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { currentBuild.result = 'NOT_BUILT' return } -def throttleCount -// Don't build the TCK on PRs, unless they use the tck label -if ( env.CHANGE_ID != null ) { - if ( !pullRequest.labels.contains( 'tck' ) ) { - print "INFO: Build skipped because pull request doesn't have 'tck' label" - return - } - throttleCount = 20 -} -else { - throttleCount = 1 +// This is a limited maintenance branch, so don't run this on pushes to the branch, only on PRs +if ( !env.CHANGE_ID ) { + print "INFO: Build skipped because this job should only run for pull request, not for branch pushes" + currentBuild.result = 'NOT_BUILT' + return } pipeline { @@ -25,7 +19,6 @@ pipeline { jdk 'OpenJDK 21 Latest' } options { - rateLimitBuilds(throttle: [count: throttleCount, durationName: 'day', userBoost: true]) buildDiscarder(logRotator(numToKeepStr: '3', artifactNumToKeepStr: '3')) disableConcurrentBuilds(abortPrevious: true) } diff --git a/ci/quarkus.Jenkinsfile b/ci/quarkus.Jenkinsfile new file mode 100644 index 000000000000..16d83b684d81 --- /dev/null +++ b/ci/quarkus.Jenkinsfile @@ -0,0 +1,84 @@ +@Library('hibernate-jenkins-pipeline-helpers') _ + +// Avoid running the pipeline on branch indexing +if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { + print "INFO: Build skipped due to trigger being Branch Indexing" + currentBuild.result = 'NOT_BUILT' + return +} +// This is a limited maintenance branch, so don't run this on pushes to the branch, only on PRs +if ( !env.CHANGE_ID ) { + print "INFO: Build skipped because this job should only run for pull request, not for branch pushes" + currentBuild.result = 'NOT_BUILT' + return +} + +pipeline { + agent none + tools { + jdk 'OpenJDK 21 Latest' + maven 'Apache Maven 3.9' + } + options { + buildDiscarder(logRotator(numToKeepStr: '3', artifactNumToKeepStr: '3')) + disableConcurrentBuilds(abortPrevious: true) + skipDefaultCheckout() + } + stages { + stage('Checks') { + steps { + requireApprovalForPullRequest 'hibernate' + } + } + stage('Build') { + agent { + label 'LongDuration' + } + steps { + script { + dir('hibernate') { + checkout scm + sh "./gradlew clean publishToMavenLocal -x test --no-scan --no-daemon --no-build-cache --stacktrace -PmavenMirror=nexus-load-balancer-c4cf05fd92f43ef8.elb.us-east-1.amazonaws.com -Dmaven.repo.local=${env.WORKSPACE}/.m2repository" + script { + env.HIBERNATE_VERSION = sh ( + script: "grep hibernateVersion gradle/version.properties|cut -d'=' -f2", + returnStdout: true + ).trim() + } + } + dir('quarkus') { + def quarkusVersionToTest = '3.27' + sh "git clone -b ${quarkusVersionToTest} --single-branch https://github.com/quarkusio/quarkus.git . || git reset --hard && git clean -fx && git pull" + script { + def sedStatus = sh (script: "sed -i 's@.*@${env.HIBERNATE_VERSION}@' pom.xml", returnStatus: true) + if ( sedStatus != 0 ) { + throw new IllegalArgumentException( "Unable to replace hibernate version in Quarkus pom. Got exit code $sedStatus" ) + } + } + // Need to override the default maven configuration this way, because there is no other way to do it + sh "sed -i 's/-Xmx5g/-Xmx2048m/' ./.mvn/jvm.config" + sh "echo -e '\\n-XX:MaxMetaspaceSize=1024m'>>./.mvn/jvm.config" + withMaven(mavenLocalRepo: env.WORKSPACE + '/.m2repository', publisherStrategy: 'EXPLICIT') { + // to account for script-only maven wrapper use in Quarkus: + withEnv(["MAVEN_ARGS=${env.MAVEN_ARGS?:""} ${env.MAVEN_CONFIG}"]) { + sh "./mvnw -pl !docs -Dquickly install" + // Need to kill the gradle daemons started during the Maven install run + sh "sudo pkill -f '.*GradleDaemon.*' || true" + // Need to override the default maven configuration this way, because there is no other way to do it + sh "sed -i 's/-Xmx2048m/-Xmx1340m/' ./.mvn/jvm.config" + sh "sed -i 's/MaxMetaspaceSize=1024m/MaxMetaspaceSize=512m/' ./.mvn/jvm.config" + def excludes = "'!integration-tests/kafka-oauth-keycloak,!integration-tests/kafka-sasl-elytron,!integration-tests/hibernate-search-orm-opensearch,!integration-tests/maven,!integration-tests/quartz,!integration-tests/reactive-messaging-kafka,!integration-tests/resteasy-reactive-kotlin/standard,!integration-tests/opentelemetry-reactive-messaging,!integration-tests/virtual-threads/kafka-virtual-threads,!integration-tests/smallrye-jwt-oidc-webapp,!extensions/oidc-db-token-state-manager/deployment,!docs'" + sh "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true ./mvnw -Dinsecure.repositories=WARN -pl :quarkus-hibernate-orm -amd -pl ${excludes} verify -Dstart-containers -Dtest-containers -Dskip.gradle.build" + } + } + } + } + } + } + } + post { + always { + notifyBuildResult maintainers: "andrea@hibernate.org steve@hibernate.org christian.beikov@gmail.com mbellade@redhat.com" + } + } +} diff --git a/ci/release/Jenkinsfile b/ci/release/Jenkinsfile index a6c8f6f7aa14..e64b10e7fbab 100644 --- a/ci/release/Jenkinsfile +++ b/ci/release/Jenkinsfile @@ -15,7 +15,7 @@ import org.hibernate.jenkins.pipeline.helpers.version.Version // Global build configuration env.PROJECT = "orm" env.JIRA_KEY = "HHH" -def RELEASE_ON_SCHEDULE = false // Set to `true` *only* on branches where you want a scheduled release. +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}" @@ -216,7 +216,7 @@ pipeline { 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']) { + sshagent(['ed25519.Hibernate-CI.github.com', 'hibernate.filemgmt.jboss.org', 'jenkins.in.relation.to', 'hibernate-ci.frs.sourceforge.net']) { // performs documentation upload and Sonatype release // push to github withEnv([ @@ -255,12 +255,15 @@ pipeline { ]) { sshagent( ['ed25519.Hibernate-CI.github.com', 'hibernate.filemgmt.jboss.org', 'hibernate-ci.frs.sourceforge.net'] ) { 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}" + // Lock to avoid rejected pushes when multiple releases try to clone-commit-push + lock('hibernate.org-git') { + 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}" + } } } } diff --git a/gradle/gradle-develocity.gradle b/gradle/gradle-develocity.gradle index 5c7416f3aaf3..4e1db29d877b 100644 --- a/gradle/gradle-develocity.gradle +++ b/gradle/gradle-develocity.gradle @@ -9,8 +9,8 @@ ext { isCiEnvironment = isJenkins() || isGitHubActions() || isGenericCi() - populateRemoteBuildCache = isEnabled( "POPULATE_REMOTE_GRADLE_CACHE" ) - useRemoteCache = !isEnabled( "DISABLE_REMOTE_GRADLE_CACHE" ) + populateRemoteBuildCache = getSetting('POPULATE_REMOTE_GRADLE_CACHE').orElse('false').toBoolean() + useRemoteCache = !getSetting('DISABLE_REMOTE_GRADLE_CACHE').orElse('false').toBoolean() } private static boolean isJenkins() { @@ -35,14 +35,6 @@ static java.util.Optional getSetting(String name) { return java.util.Optional.ofNullable(sysProp); } -static boolean isEnabled(String setting) { - if ( System.getenv().hasProperty( setting ) ) { - return true - } - - return System.hasProperty( setting ) -} - develocity { server = 'https://develocity.commonhaus.dev' diff --git a/gradle/version.properties b/gradle/version.properties index 12db13d4230a..09e1e1f372fc 100644 --- a/gradle/version.properties +++ b/gradle/version.properties @@ -1 +1 @@ -hibernateVersion=7.1.1-SNAPSHOT \ No newline at end of file +hibernateVersion=7.1.5-SNAPSHOT \ No newline at end of file diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index 80dc0affc598..c31ceca11f18 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java @@ -75,7 +75,6 @@ import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.ClobJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; -import org.hibernate.type.descriptor.jdbc.NClobJdbcType; import org.hibernate.type.descriptor.jdbc.ObjectNullAsBinaryTypeJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; @@ -421,7 +420,7 @@ protected void contributeCockroachTypes(TypeContributions typeContributions, Ser // Force Blob binding to byte[] for CockroachDB jdbcTypeRegistry.addDescriptor( Types.BLOB, BlobJdbcType.MATERIALIZED ); jdbcTypeRegistry.addDescriptor( Types.CLOB, ClobJdbcType.MATERIALIZED ); - jdbcTypeRegistry.addDescriptor( Types.NCLOB, NClobJdbcType.MATERIALIZED ); + jdbcTypeRegistry.addDescriptor( Types.NCLOB, ClobJdbcType.MATERIALIZED ); // The next two contributions are the same as for Postgresql typeContributions.contributeJdbcType( ObjectNullAsBinaryTypeJdbcType.INSTANCE ); @@ -1095,6 +1094,8 @@ public LockingSupport getLockingSupport() { @Override public boolean useInputStreamToInsertBlob() { + // PG-JDBC treats setBinaryStream()/setCharacterStream() calls like bytea/varchar, which are not LOBs, + // so disable stream bindings for this dialect completely return false; } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java index 7b165775a200..0ec069c3a8d5 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java @@ -1095,7 +1095,16 @@ public boolean supportsLobValueChangePropagation() { } @Override - public boolean useInputStreamToInsertBlob() { + public boolean useConnectionToCreateLob() { + return false; + } + + @Override + public boolean supportsNationalizedMethods() { + // See HHH-12753, HHH-18314, HHH-19201 + // Old DB2 JDBC drivers do not support setNClob, setNCharcterStream or setNString. + // In more recent driver versions, some methods just delegate to the non-N variant, but others still fail. + // Ultimately, let's just avoid the N variant methods on DB2 altogether return false; } @@ -1490,4 +1499,9 @@ public boolean supportsRowValueConstructorSyntaxInInList() { return false; } + @Override + public boolean supportsRowValueConstructorSyntaxInInSubQuery() { + return true; + } + } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java index 9bc6298e6c47..60e821c0a973 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java @@ -205,7 +205,7 @@ public HANALegacyDialect(HANAServerConfiguration configuration, boolean defaultT this.maxLobPrefetchSize = configuration.getMaxLobPrefetchSize(); this.useUnicodeStringTypes = useUnicodeStringTypesDefault(); - this.lockingSupport = buildLockingSupport(); + this.lockingSupport = HANALockingSupport.forDialectVersion( configuration.getFullVersion() ); } private LockingSupport buildLockingSupport() { @@ -998,34 +998,18 @@ public String getReadLockString(Timeout timeout) { } @Override - public String getReadLockString(String aliases, Timeout timeout) { - return getWriteLockString( aliases, timeout ); + public String getForUpdateString(Timeout timeout) { + return withTimeout( getForUpdateString(), timeout.milliseconds() ); } @Override - public String getWriteLockString(Timeout timeout) { - if ( Timeouts.isRealTimeout( timeout ) ) { - return getForUpdateString() + " wait " + getTimeoutInSeconds( timeout.milliseconds() ); - } - else if ( timeout.milliseconds() == Timeouts.NO_WAIT_MILLI ) { - return getForUpdateNowaitString(); - } - else { - return getForUpdateString(); - } + public String getReadLockString(String aliases, Timeout timeout) { + return getWriteLockString( aliases, timeout ); } @Override public String getWriteLockString(String aliases, Timeout timeout) { - if ( Timeouts.isRealTimeout( timeout ) ) { - return getForUpdateString( aliases ) + " wait " + getTimeoutInSeconds( timeout.milliseconds() ); - } - else if ( timeout.milliseconds() == Timeouts.NO_WAIT_MILLI ) { - return getForUpdateNowaitString( aliases ); - } - else { - return getForUpdateString( aliases ); - } + return withTimeout( getForUpdateString( aliases ), timeout.milliseconds() ); } @Override @@ -1039,29 +1023,17 @@ public String getReadLockString(String aliases, int timeout) { } @Override - public String getWriteLockString(int timeout) { - if ( Timeouts.isRealTimeout( timeout ) ) { - return getForUpdateString() + " wait " + Timeouts.getTimeoutInSeconds( timeout ); - } - else if ( timeout == Timeouts.NO_WAIT_MILLI ) { - return getForUpdateNowaitString(); - } - else { - return getForUpdateString(); - } + public String getWriteLockString(String aliases, int timeout) { + return withTimeout( getForUpdateString( aliases ), timeout ); } - @Override - public String getWriteLockString(String aliases, int timeout) { - if ( timeout > 0 ) { - return getForUpdateString( aliases ) + " wait " + getTimeoutInSeconds( timeout ); - } - else if ( timeout == 0 ) { - return getForUpdateNowaitString( aliases ); - } - else { - return getForUpdateString( aliases ); - } + private String withTimeout(String lockString, int timeout) { + return switch (timeout) { + case Timeouts.NO_WAIT_MILLI -> supportsNoWait() ? lockString + " nowait" : lockString; + case Timeouts.SKIP_LOCKED_MILLI -> supportsSkipLocked() ? lockString + SQL_IGNORE_LOCKED : lockString; + case Timeouts.WAIT_FOREVER_MILLI -> lockString; + default -> supportsWait() ? lockString + " wait " + getTimeoutInSeconds( timeout ) : lockString; + }; } @Override @@ -2006,11 +1978,6 @@ public String getForUpdateSkipLockedString(String aliases) { getForUpdateString(aliases) + SQL_IGNORE_LOCKED : getForUpdateString(aliases); } - @Override - public String getForUpdateString(LockMode lockMode) { - return super.getForUpdateString(lockMode); - } - @Override public String getDual() { return "sys.dummy"; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java index 36bc428ee2d8..7a94c092e662 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java @@ -912,6 +912,7 @@ public boolean supportsArrayConstructor() { @Override public boolean supportsRowValueConstructorSyntax() { + // It's supported but not usable due to a bug: https://sourceforge.net/p/hsqldb/bugs/1714/ return false; } @@ -923,11 +924,13 @@ public boolean supportsWithClauseInSubquery() { @Override public boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { + // It's supported but not usable due to a bug: https://sourceforge.net/p/hsqldb/bugs/1714/ return false; } @Override public boolean supportsRowValueConstructorSyntaxInInList() { + // It's supported but not usable due to a bug: https://sourceforge.net/p/hsqldb/bugs/1714/ return false; } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java index 6f5d4e6e702e..f9ef9ec3ca50 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java @@ -216,6 +216,11 @@ public boolean supportsColumnCheck() { return getVersion().isSameOrAfter( 10, 2 ); } + @Override + public boolean supportsNamedColumnCheck() { + return false; + } + @Override public boolean doesRoundTemporalOnOverflow() { // See https://jira.mariadb.org/browse/MDEV-16991 diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index 130f86be7bef..10fd6b11e46a 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java @@ -191,16 +191,18 @@ public MySQLLegacyDialect(DialectResolutionInfo info) { protected static DatabaseVersion createVersion(DialectResolutionInfo info) { final String versionString = info.getDatabaseVersion(); - final String[] components = StringHelper.split( ".", versionString ); - if ( components.length >= 3 ) { - try { - final int majorVersion = Integer.parseInt( components[0] ); - final int minorVersion = Integer.parseInt( components[1] ); - final int patchLevel = Integer.parseInt( components[2] ); - return DatabaseVersion.make( majorVersion, minorVersion, patchLevel ); - } - catch (NumberFormatException ex) { - // Ignore + if ( versionString != null ) { + final String[] components = StringHelper.split( ".", versionString ); + if ( components.length >= 3 ) { + try { + final int majorVersion = Integer.parseInt( components[0] ); + final int minorVersion = Integer.parseInt( components[1] ); + final int patchLevel = Integer.parseInt( components[2] ); + return DatabaseVersion.make( majorVersion, minorVersion, patchLevel ); + } + catch (NumberFormatException ex) { + // Ignore + } } } return info.makeCopyOrDefault( DEFAULT_VERSION ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index 91c59b8af375..0d850ca13620 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -29,6 +29,7 @@ import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.DmlTargetColumnQualifierSupport; +import org.hibernate.dialect.OracleServerConfiguration; import org.hibernate.dialect.temptable.OracleLocalTemporaryTableStrategy; import org.hibernate.dialect.temptable.StandardGlobalTemporaryTableStrategy; import org.hibernate.dialect.temptable.TemporaryTableStrategy; @@ -206,6 +207,18 @@ protected void applyAggregateColumnCheck(StringBuilder buf, AggregateColumn aggr } }; + // Is it an Autonomous Database Cloud Service? + protected final boolean autonomous; + + // Is MAX_STRING_SIZE set to EXTENDED? + protected final boolean extended; + + // Is the database accessed using a database service protected by Application Continuity. + protected final boolean applicationContinuity; + + protected final int driverMajorVersion; + protected final int driverMinorVersion; + private final LockingSupport lockingSupport; public OracleLegacyDialect() { @@ -213,13 +226,39 @@ public OracleLegacyDialect() { } public OracleLegacyDialect(DatabaseVersion version) { - super(version); + super( version ); lockingSupport = new OracleLockingSupport( version ); + autonomous = false; + extended = false; + applicationContinuity = false; + driverMajorVersion = 19; + driverMinorVersion = 0; } public OracleLegacyDialect(DialectResolutionInfo info) { - super(info); + this( info, OracleServerConfiguration.fromDialectResolutionInfo( info ) ); + } + + public OracleLegacyDialect(DialectResolutionInfo info, OracleServerConfiguration serverConfiguration) { + super( info ); lockingSupport = new OracleLockingSupport( getVersion() ); + autonomous = serverConfiguration.isAutonomous(); + extended = serverConfiguration.isExtended(); + applicationContinuity = serverConfiguration.isApplicationContinuity(); + this.driverMinorVersion = serverConfiguration.getDriverMinorVersion(); + this.driverMajorVersion = serverConfiguration.getDriverMajorVersion(); + } + + public boolean isAutonomous() { + return autonomous; + } + + public boolean isExtended() { + return extended; + } + + public boolean isApplicationContinuity() { + return applicationContinuity; } @Override @@ -1661,11 +1700,11 @@ public boolean supportsFromClauseInUpdate() { @Override public boolean useInputStreamToInsertBlob() { - // see HHH-18206 - return false; + // If application continuity is enabled, don't use stream bindings, since a replay could otherwise fail + // if the underlying stream doesn't support mark and reset + return !isApplicationContinuity(); } - @Override public String appendCheckConstraintOptions(CheckConstraint checkConstraint, String sqlCheckConstraint) { if ( StringHelper.isNotEmpty( checkConstraint.getOptions() ) ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index 91debe50d7dd..8e99619e5be3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java @@ -944,6 +944,8 @@ public GenerationType getNativeValueGenerationStrategy() { @Override public boolean useInputStreamToInsertBlob() { + // PG-JDBC treats setBinaryStream()/setCharacterStream() calls like bytea/varchar, which are not LOBs, + // so disable stream bindings for this dialect completely return false; } diff --git a/hibernate-core/hibernate-core.gradle b/hibernate-core/hibernate-core.gradle index a417f71604bd..0a6dc251e8da 100644 --- a/hibernate-core/hibernate-core.gradle +++ b/hibernate-core/hibernate-core.gradle @@ -63,7 +63,13 @@ dependencies { testRuntimeOnly libs.byteBuddy testRuntimeOnly testLibs.weld - testRuntimeOnly testLibs.wildFlyTxnClient + implementation(libs.logging) + testRuntimeOnly(testLibs.wildFlyTxnClient) { + // WildFly Elytron includes a whole copy of JBoss Logging, Jackson with its original package, + // not even shaded, which causes lots of problems. + // Since we don't need WildFly Elytron in our tests, we'll just exclude it. + exclude group: 'org.wildfly.security', module: 'wildfly-elytron' + } testImplementation libs.jandex testImplementation jakartaLibs.jsonb testImplementation libs.jackson diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumn.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumn.java index f0385ea2ab6f..6bf98fddaf22 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumn.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumn.java @@ -967,7 +967,7 @@ void applyCheckConstraints(jakarta.persistence.CheckConstraint[] checkConstraint if ( isNotEmpty( checkConstraintAnnotationUsages ) ) { for ( jakarta.persistence.CheckConstraint checkConstraintAnnotationUsage : checkConstraintAnnotationUsages ) { addCheckConstraint( - checkConstraintAnnotationUsage.name(), + nullIfEmpty( checkConstraintAnnotationUsage.name() ), checkConstraintAnnotationUsage.constraint(), checkConstraintAnnotationUsage.options() ); @@ -983,7 +983,7 @@ void applyCheckConstraint(PropertyData inferredData, int length) { if ( checksAnn != null ) { final Check[] checkAnns = checksAnn.value(); for ( Check checkAnn : checkAnns ) { - addCheckConstraint( checkAnn.name(), checkAnn.constraints() ); + addCheckConstraint( nullIfEmpty( checkAnn.name() ), checkAnn.constraints() ); } } else { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BasicValueBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BasicValueBinder.java index 86123b42ac2d..7375e17eaa68 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BasicValueBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BasicValueBinder.java @@ -332,24 +332,25 @@ public void setType( if ( converterDescriptor != null ) { applyJpaConverter( value, converterDescriptor ); } - - final Class> userTypeImpl = - kind.mappingAccess.customType( value, getSourceModelContext() ); - if ( userTypeImpl != null ) { - applyExplicitType( userTypeImpl, - kind.mappingAccess.customTypeParameters( value, getSourceModelContext() ) ); - // An explicit custom UserType has top precedence when we get to BasicValue resolution. - return; - } - else if ( modelClassDetails != null ) { - final ClassDetails rawClassDetails = modelClassDetails.determineRawClass(); - final Class basicClass = rawClassDetails.toJavaClass(); - final Class> registeredUserTypeImpl = - getMetadataCollector().findRegisteredUserType( basicClass ); - if ( registeredUserTypeImpl != null ) { - applyExplicitType( registeredUserTypeImpl, emptyMap() ); + else { + final Class> userTypeImpl = + kind.mappingAccess.customType( value, getSourceModelContext() ); + if ( userTypeImpl != null ) { + applyExplicitType( userTypeImpl, + kind.mappingAccess.customTypeParameters( value, getSourceModelContext() ) ); + // An explicit custom UserType has top precedence when we get to BasicValue resolution. return; } + else if ( modelClassDetails != null ) { + final ClassDetails rawClassDetails = modelClassDetails.determineRawClass(); + final Class basicClass = rawClassDetails.toJavaClass(); + final Class> registeredUserTypeImpl = + getMetadataCollector().findRegisteredUserType( basicClass ); + if ( registeredUserTypeImpl != null ) { + applyExplicitType( registeredUserTypeImpl, emptyMap() ); + return; + } + } } switch ( kind ) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java index 229646b5bf95..406e643a19e4 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java @@ -50,6 +50,7 @@ import org.hibernate.mapping.Selectable; import org.hibernate.mapping.SimpleValue; import org.hibernate.mapping.Table; +import org.hibernate.mapping.ToOne; import org.hibernate.mapping.Value; import org.hibernate.metamodel.CollectionClassification; import org.hibernate.metamodel.UnsupportedMappingException; @@ -2747,6 +2748,13 @@ private void bindUnownedManyToManyInverseForeignKey( manyToOne.setReferencedPropertyName( referencedPropertyName ); metadataCollector.addUniquePropertyReference( targetEntity.getEntityName(), referencedPropertyName ); } + // Ensure that we copy over the delete action from the owner side before creating the foreign key + if ( property.getValue() instanceof Collection collectionValue ) { + manyToOne.setOnDeleteAction( ( (SimpleValue) collectionValue.getKey() ).getOnDeleteAction() ); + } + else if ( property.getValue() instanceof ToOne toOne ) { + manyToOne.setOnDeleteAction( toOne.getOnDeleteAction() ); + } manyToOne.setReferenceToPrimaryKey( referencedPropertyName == null ); value.createForeignKey(); } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java index d0b195452b2a..3fc9cbafe5ca 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java @@ -988,7 +988,9 @@ static Component createEmbeddable( component.setComponentClassName( component.getOwner().getClassName() ); } else { - component.setComponentClassName( inferredData.getClassOrElementType().getName() ); + final TypeDetails type = inferredData.getClassOrElementType(); + component.setComponentClassName( type.getName() ); + checkEmbeddableRecursiveHierarchy( type, inferredData, propertyHolder ); } component.setCustomInstantiator( customInstantiatorImpl ); final Constructor constructor = resolveInstantiator( inferredData.getClassOrElementType() ); @@ -1003,6 +1005,30 @@ static Component createEmbeddable( return component; } + private static void checkEmbeddableRecursiveHierarchy( + TypeDetails type, + PropertyData propertyData, + PropertyHolder propertyHolder) { + final ClassDetails embeddableClass = type.determineRawClass(); + while ( propertyHolder.isComponent() ) { + final ComponentPropertyHolder componentHolder = (ComponentPropertyHolder) propertyHolder; + // we need to check that the embeddable is not used in a recursive hierarchy + ClassDetails classDetails = embeddableClass; + while ( classDetails != null ) { + if ( propertyHolder.getClassName().equals( classDetails.getClassName() ) ) { + throw new MappingException( String.format( + Locale.ROOT, + "Recursive embeddable mapping detected for property '%s' for type [%s]", + getPath( propertyHolder, propertyData ), + propertyHolder.getClassName() + ) ); + } + classDetails = classDetails.getSuperClass(); + } + propertyHolder = componentHolder.parent; + } + } + private static void applyColumnNamingPattern(Component component, PropertyData inferredData) { final Class componentClass = component.getComponentClass(); if ( componentClass == null || Map.class.equals( componentClass ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/TableBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/TableBinder.java index ed807176d6b2..8392cf29716a 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/TableBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/TableBinder.java @@ -44,6 +44,7 @@ import static org.hibernate.internal.util.StringHelper.isNotEmpty; import static org.hibernate.internal.util.StringHelper.isQuoted; import static org.hibernate.internal.util.StringHelper.nullIfBlank; +import static org.hibernate.internal.util.StringHelper.nullIfEmpty; import static org.hibernate.internal.util.StringHelper.unquote; import static org.hibernate.internal.util.collections.CollectionHelper.isNotEmpty; @@ -896,7 +897,7 @@ static void addTableCheck( for ( jakarta.persistence.CheckConstraint checkConstraintAnnotationUsage : checkConstraintAnnotationUsages ) { table.addCheck( new CheckConstraint( - checkConstraintAnnotationUsage.name(), + nullIfEmpty( checkConstraintAnnotationUsage.name() ), checkConstraintAnnotationUsage.constraint(), checkConstraintAnnotationUsage.options() ) diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java index 133b5b624aea..4a5177598134 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java @@ -823,8 +823,23 @@ String getDescriptor() { return fieldDescription.getDescriptor(); } - boolean isVisibleTo(TypeDescription typeDescription) { - return fieldDescription.isVisibleTo( typeDescription ); + boolean isVisibleTo(TypeDescription type) { + final var declaringType = fieldDescription.getDeclaringType().asErasure(); + if ( declaringType.isVisibleTo( type ) ) { + if ( fieldDescription.isPublic() || type.equals( declaringType ) ) { + return true; + } + else if ( fieldDescription.isProtected() ) { + return declaringType.isAssignableFrom( type ); + } + else if ( fieldDescription.isPrivate() ) { + return type.isNestMateOf( declaringType ); + } + // We explicitly consider package-private fields as not visible, as the classes + // might have the same package name but be loaded by different class loaders. + // (see https://hibernate.atlassian.net/browse/HHH-19784) + } + return false; } FieldDescription getFieldDescription() { diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java index 23b1030963d9..392a6f2622a9 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java @@ -300,6 +300,13 @@ public boolean hasWrittenFieldNames() { return writtenFieldNames != null && !writtenFieldNames.isEmpty(); } + /* + * Used by Hibernate Reactive + */ + protected boolean isIdentifier(String attributeName) { + return meta.identifierAttributeNames.contains( attributeName ); + } + private enum Status { UNINITIALIZED, INITIALIZING, diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/BytecodeEnhancementMetadataPojoImpl.java b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/BytecodeEnhancementMetadataPojoImpl.java index c4926385fba7..ee468b88ceac 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/BytecodeEnhancementMetadataPojoImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/BytecodeEnhancementMetadataPojoImpl.java @@ -40,11 +40,11 @@ * * @author Steve Ebersole */ -public final class BytecodeEnhancementMetadataPojoImpl implements BytecodeEnhancementMetadata { +public class BytecodeEnhancementMetadataPojoImpl implements BytecodeEnhancementMetadata { /** * Static constructor */ - public static BytecodeEnhancementMetadata from( + public static BytecodeEnhancementMetadataPojoImpl from( PersistentClass persistentClass, Set identifierAttributeNames, CompositeType nonAggregatedCidMapper, @@ -75,7 +75,10 @@ public static BytecodeEnhancementMetadata from( private final LazyAttributeLoadingInterceptor.EntityRelatedState lazyAttributeLoadingInterceptorState; private volatile transient EnhancementAsProxyLazinessInterceptor.EntityRelatedState enhancementAsProxyInterceptorState; - BytecodeEnhancementMetadataPojoImpl( + /* + * Used by Hibernate Reactive + */ + protected BytecodeEnhancementMetadataPojoImpl( String entityName, Class entityClass, Set identifierAttributeNames, @@ -248,9 +251,12 @@ public void injectEnhancedEntityAsProxyInterceptor( ); } + /* + * Used by Hibernate Reactive + */ //This state object needs to be lazily initialized as it needs access to the Persister, but once //initialized it can be reused across multiple sessions. - private EnhancementAsProxyLazinessInterceptor.EntityRelatedState getEnhancementAsProxyLazinessInterceptorMetastate(SharedSessionContractImplementor session) { + public EnhancementAsProxyLazinessInterceptor.EntityRelatedState getEnhancementAsProxyLazinessInterceptorMetastate(SharedSessionContractImplementor session) { EnhancementAsProxyLazinessInterceptor.EntityRelatedState state = this.enhancementAsProxyInterceptorState; if ( state == null ) { final EntityPersister entityPersister = session.getFactory().getMappingMetamodel() @@ -311,4 +317,17 @@ public void injectInterceptor( return (BytecodeLazyAttributeInterceptor) interceptor; } + /* + * Used by Hibernate Reactive + */ + public Class getEntityClass() { + return entityClass; + } + + /* + * Used by Hibernate Reactive + */ + public LazyAttributeLoadingInterceptor.EntityRelatedState getLazyAttributeLoadingInterceptorState() { + return lazyAttributeLoadingInterceptorState; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/Environment.java b/hibernate-core/src/main/java/org/hibernate/cfg/Environment.java index eeea43cc61ea..7db4c9fa779b 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/Environment.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/Environment.java @@ -144,7 +144,8 @@ public final class Environment implements AvailableSettings { InputStream stream = ConfigHelper.getResourceAsStream( "/hibernate.properties" ); try { GLOBAL_PROPERTIES.load(stream); - LOG.propertiesLoaded( ConfigurationHelper.maskOut( GLOBAL_PROPERTIES, PASS ) ); + LOG.propertiesLoaded( ConfigurationHelper.maskOut( GLOBAL_PROPERTIES, + PASS, JAKARTA_JDBC_PASSWORD, JPA_JDBC_PASSWORD ) ); } catch (Exception e) { LOG.unableToLoadProperties(); diff --git a/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java b/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java index 8325b338afe5..04cf40d47508 100644 --- a/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java +++ b/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java @@ -859,14 +859,14 @@ public final boolean hasQueuedOperations() { } @Override - public final Iterator queuedAdditionIterator() { + public final Iterator queuedAdditionIterator() { if ( hasQueuedOperations() ) { return new Iterator<>() { private int index; @Override - public E next() { - return operationQueue.get( index++ ).getAddedInstance(); + public Object next() { + return operationQueue.get( index++ ).getAddedEntry(); } @Override @@ -1241,6 +1241,10 @@ protected interface DelayedOperation { E getAddedInstance(); + default Object getAddedEntry() { + return getAddedInstance(); + } + E getOrphan(); } diff --git a/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentCollection.java b/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentCollection.java index 80326021ddf5..22c3d7134a29 100644 --- a/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentCollection.java +++ b/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentCollection.java @@ -399,7 +399,7 @@ default boolean needsUpdating( * * @return The iterator */ - Iterator queuedAdditionIterator(); + Iterator queuedAdditionIterator(); /** * Get the "queued" orphans diff --git a/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentMap.java b/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentMap.java index 3fd2b790a45b..3082cdb10e2b 100644 --- a/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentMap.java +++ b/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentMap.java @@ -536,6 +536,11 @@ protected AbstractMapValueDelayedOperation(K index, E addedValue, E orphan) { protected final K getIndex() { return index; } + + @Override + public Object getAddedEntry() { + return Map.entry( getIndex(), getAddedInstance() ); + } } final class Put extends AbstractMapValueDelayedOperation { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index 440cc014f3c6..c22651cedd7d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -67,7 +67,6 @@ import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.ClobJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; -import org.hibernate.type.descriptor.jdbc.NClobJdbcType; import org.hibernate.type.descriptor.jdbc.ObjectNullAsBinaryTypeJdbcType; import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; @@ -384,7 +383,7 @@ protected void contributeCockroachTypes(TypeContributions typeContributions, Ser // Force Blob binding to byte[] for CockroachDB jdbcTypeRegistry.addDescriptor( Types.BLOB, BlobJdbcType.MATERIALIZED ); jdbcTypeRegistry.addDescriptor( Types.CLOB, ClobJdbcType.MATERIALIZED ); - jdbcTypeRegistry.addDescriptor( Types.NCLOB, NClobJdbcType.MATERIALIZED ); + jdbcTypeRegistry.addDescriptor( Types.NCLOB, ClobJdbcType.MATERIALIZED ); // The next two contributions are the same as for Postgresql typeContributions.contributeJdbcType( ObjectNullAsBinaryTypeJdbcType.INSTANCE ); @@ -993,6 +992,8 @@ public String getForUpdateSkipLockedString(String aliases) { @Override public boolean useInputStreamToInsertBlob() { + // PG-JDBC treats setBinaryStream()/setCharacterStream() calls like bytea/varchar, which are not LOBs, + // so disable stream bindings for this dialect completely return false; } @@ -1006,6 +1007,11 @@ public boolean supportsOffsetInSubquery() { return true; } + @Override + public boolean supportsInsertReturning() { + return true; + } + @Override public boolean supportsWindowFunctions() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java index b879a94a7f3c..6c83de727ea2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -895,7 +895,16 @@ public boolean supportsLobValueChangePropagation() { } @Override - public boolean useInputStreamToInsertBlob() { + public boolean useConnectionToCreateLob() { + return false; + } + + @Override + public boolean supportsNationalizedMethods() { + // See HHH-12753, HHH-18314, HHH-19201 + // Old DB2 JDBC drivers do not support setNClob, setNCharcterStream or setNString. + // In more recent driver versions, some methods just delegate to the non-N variant, but others still fail. + // Ultimately, let's just avoid the N variant methods on DB2 altogether return false; } @@ -1264,4 +1273,9 @@ public boolean supportsRowValueConstructorSyntaxInInList() { return false; } + @Override + public boolean supportsRowValueConstructorSyntaxInInSubQuery() { + return true; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java index 29135c068c74..f4f6a9fffb4a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java @@ -113,7 +113,7 @@ public SequenceSupport getSequenceSupport() { @Override public String getQuerySequencesString() { - return "select * from sysibm.syssequences"; + return "select case when seqtype='A' then seqschema else schema end as seqschema, case when seqtype='A' then seqname else name end as seqname, start, minvalue, maxvalue, increment from sysibm.syssequences"; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index c6f14ad203d9..7588cdf789d6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -337,7 +337,8 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun private static final Pattern ESCAPE_CLOSING_COMMENT_PATTERN = Pattern.compile( "\\*/" ); private static final Pattern ESCAPE_OPENING_COMMENT_PATTERN = Pattern.compile( "/\\*" ); private static final Pattern QUERY_PATTERN = Pattern.compile( - "^\\s*(select\\b.+?\\bfrom\\b.+?)(\\b(?:natural )?(?:left |right |full )?(?:inner |outer |cross )?join.+?\\b)?(\\bwhere\\b.+?)$"); + "^\\s*(select\\b.+?\\bfrom\\b.+?)(\\b(?:natural )?(?:left |right |full )?(?:inner |outer |cross )?join.+?\\b)?(\\bwhere\\b.+?)$", + Pattern.CASE_INSENSITIVE); private static final CoreMessageLogger LOG = Logger.getMessageLogger( MethodHandles.lookup(), CoreMessageLogger.class, Dialect.class.getName() ); @@ -4186,6 +4187,16 @@ public boolean supportsColumnCheck() { return true; } + /** + * Does this dialect support named column-level check constraints? + * + * @return True if named column-level {@code check} constraints are supported; + * false otherwise. + */ + public boolean supportsNamedColumnCheck() { + return supportsColumnCheck(); + } + /** * Does this dialect support table-level check constraints? * diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java index 613ea3390a85..3a821bceaacf 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -1003,34 +1003,18 @@ public String getReadLockString(Timeout timeout) { } @Override - public String getReadLockString(String aliases, Timeout timeout) { - return getWriteLockString( aliases, timeout ); + public String getForUpdateString(Timeout timeout) { + return withTimeout( getForUpdateString(), timeout.milliseconds() ); } @Override - public String getWriteLockString(Timeout timeout) { - if ( Timeouts.isRealTimeout( timeout ) ) { - return getForUpdateString() + " wait " + Timeouts.getTimeoutInSeconds( timeout.milliseconds() ); - } - else if ( timeout.milliseconds() == Timeouts.NO_WAIT_MILLI ) { - return getForUpdateNowaitString(); - } - else { - return getForUpdateString(); - } + public String getReadLockString(String aliases, Timeout timeout) { + return getWriteLockString( aliases, timeout ); } @Override public String getWriteLockString(String aliases, Timeout timeout) { - if ( Timeouts.isRealTimeout( timeout ) ) { - return getForUpdateString( aliases ) + " wait " + getTimeoutInSeconds( timeout.milliseconds() ); - } - else if ( timeout.milliseconds() == Timeouts.NO_WAIT_MILLI ) { - return getForUpdateNowaitString( aliases ); - } - else { - return getForUpdateString( aliases ); - } + return withTimeout( getForUpdateString( aliases ), timeout.milliseconds() ); } @Override @@ -1044,29 +1028,17 @@ public String getReadLockString(String aliases, int timeout) { } @Override - public String getWriteLockString(int timeout) { - if ( timeout > 0 ) { - return getForUpdateString() + " wait " + getTimeoutInSeconds( timeout ); - } - else if ( timeout == Timeouts.NO_WAIT_MILLI ) { - return getForUpdateNowaitString(); - } - else { - return getForUpdateString(); - } + public String getWriteLockString(String aliases, int timeout) { + return withTimeout( getForUpdateString( aliases ), timeout ); } - @Override - public String getWriteLockString(String aliases, int timeout) { - if ( timeout > 0 ) { - return getForUpdateString( aliases ) + " wait " + getTimeoutInSeconds( timeout ); - } - else if ( timeout == 0 ) { - return getForUpdateNowaitString( aliases ); - } - else { - return getForUpdateString( aliases ); - } + private String withTimeout(String lockString, int timeout) { + return switch (timeout) { + case Timeouts.NO_WAIT_MILLI -> supportsNoWait() ? lockString + " nowait" : lockString; + case Timeouts.SKIP_LOCKED_MILLI -> supportsSkipLocked() ? lockString + SQL_IGNORE_LOCKED : lockString; + case Timeouts.WAIT_FOREVER_MILLI -> lockString; + default -> supportsWait() ? lockString + " wait " + getTimeoutInSeconds( timeout ) : lockString; + }; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java index b30dcdc651c9..571e0458d25b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -746,6 +746,7 @@ public boolean supportsArrayConstructor() { @Override public boolean supportsRowValueConstructorSyntax() { + // It's supported but not usable due to a bug: https://sourceforge.net/p/hsqldb/bugs/1714/ return false; } @@ -757,11 +758,13 @@ public boolean supportsWithClauseInSubquery() { @Override public boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { + // It's supported but not usable due to a bug: https://sourceforge.net/p/hsqldb/bugs/1714/ return false; } @Override public boolean supportsRowValueConstructorSyntaxInInList() { + // It's supported but not usable due to a bug: https://sourceforge.net/p/hsqldb/bugs/1714/ return false; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java index e2527fa8193e..cbe31addc501 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -245,6 +245,11 @@ public boolean supportsColumnCheck() { return true; } + @Override + public boolean supportsNamedColumnCheck() { + return false; + } + @Override public boolean doesRoundTemporalOnOverflow() { // See https://jira.mariadb.org/browse/MDEV-16991 diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index e9e75adcc0ef..9d2e0f5ced88 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -1810,11 +1810,11 @@ public String[] getDropEnumTypeCommand(String name) { @Override public boolean useInputStreamToInsertBlob() { - // see HHH-18206 - return false; + // If application continuity is enabled, don't use stream bindings, since a replay could otherwise fail + // if the underlying stream doesn't support mark and reset + return !isApplicationContinuity(); } - @Override public String appendCheckConstraintOptions(CheckConstraint checkConstraint, String sqlCheckConstraint) { return isNotEmpty( checkConstraint.getOptions() ) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index 54fd00b75940..87812301207b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -902,6 +902,8 @@ public GenerationType getNativeValueGenerationStrategy() { @Override public boolean useInputStreamToInsertBlob() { + // PG-JDBC treats setBinaryStream()/setCharacterStream() calls like bytea/varchar, which are not LOBs, + // so disable stream bindings for this dialect completely return false; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/CockroachLockingSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/CockroachLockingSupport.java index 6f41f9f47bab..4db473322ad2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/CockroachLockingSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/CockroachLockingSupport.java @@ -15,7 +15,6 @@ import java.sql.Connection; -import static org.hibernate.Timeouts.NO_WAIT; import static org.hibernate.Timeouts.NO_WAIT_MILLI; import static org.hibernate.Timeouts.SKIP_LOCKED_MILLI; import static org.hibernate.Timeouts.WAIT_FOREVER; @@ -82,11 +81,9 @@ public Timeout getLockTimeout(Connection connection, SessionFactoryImplementor f return Helper.getLockTimeout( "show lock_timeout", (resultSet) -> { - // see https://dev.mysql.com/doc/refman/8.4/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout final int millis = resultSet.getInt( 1 ); return switch ( millis ) { - case 0 -> NO_WAIT; - case 100000000 -> WAIT_FOREVER; + case 0 -> WAIT_FOREVER; default -> Timeout.milliseconds( millis ); }; }, @@ -103,15 +100,14 @@ public void setLockTimeout( Helper.setLockTimeout( timeout, (t) -> { - // see https://dev.mysql.com/doc/refman/8.4/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout final int milliseconds = timeout.milliseconds(); if ( milliseconds == SKIP_LOCKED_MILLI ) { throw new HibernateException( "Connection lock-timeout does not accept skip-locked" ); } - if ( milliseconds == WAIT_FOREVER_MILLI ) { - return 100000000; + if ( milliseconds == NO_WAIT_MILLI ) { + throw new HibernateException( "Connection lock-timeout does not accept no-wait" ); } - return milliseconds; + return milliseconds == WAIT_FOREVER_MILLI ? 0 : milliseconds; }, "set lock_timeout = %s", connection, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/HANALockingSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/HANALockingSupport.java index ac60c6b418f6..61fe43d857de 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/HANALockingSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/HANALockingSupport.java @@ -4,9 +4,11 @@ */ package org.hibernate.dialect.lock.internal; +import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.RowLockStrategy; import org.hibernate.dialect.lock.PessimisticLockStyle; import org.hibernate.dialect.lock.spi.ConnectionLockTimeoutStrategy; +import org.hibernate.dialect.lock.spi.LockingSupport; import org.hibernate.dialect.lock.spi.OuterJoinLockingType; /** @@ -15,14 +17,24 @@ * @author Steve Ebersole */ public class HANALockingSupport extends LockingSupportParameterized { - public static final HANALockingSupport HANA_LOCKING_SUPPORT = new HANALockingSupport( true ); + public static final HANALockingSupport HANA_LOCKING_SUPPORT = new HANALockingSupport( true, true ); + + public static LockingSupport forDialectVersion(DatabaseVersion version) { + final boolean supportsWait = version.isSameOrAfter( 2, 0, 10 ); + final boolean supportsSkipLocked = version.isSameOrAfter(2, 0, 30); + return new HANALockingSupport( supportsWait, supportsSkipLocked ); + } public HANALockingSupport(boolean supportsSkipLocked) { + this( false, supportsSkipLocked ); + } + + private HANALockingSupport(boolean supportsWait, boolean supportsSkipLocked) { super( PessimisticLockStyle.CLAUSE, RowLockStrategy.COLUMN, - false, - false, + supportsWait, + supportsWait, supportsSkipLocked, OuterJoinLockingType.IDENTIFIED ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryImpl.java index 580bb05f998c..80588cdb65de 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryImpl.java @@ -10,7 +10,6 @@ import java.io.Serializable; import org.hibernate.AssertionFailure; -import org.hibernate.CustomEntityDirtinessStrategy; import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.UnsupportedLockAttemptException; @@ -21,7 +20,6 @@ import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.ManagedEntity; import org.hibernate.engine.spi.PersistenceContext; -import org.hibernate.engine.spi.PersistentAttributeInterceptor; import org.hibernate.engine.spi.SelfDirtinessTracker; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; @@ -41,10 +39,8 @@ import static org.hibernate.engine.internal.EntityEntryImpl.EnumState.PREVIOUS_STATUS; import static org.hibernate.engine.internal.EntityEntryImpl.EnumState.STATUS; import static org.hibernate.engine.internal.ManagedTypeHelper.asManagedEntity; -import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; +import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptableOrNull; import static org.hibernate.engine.internal.ManagedTypeHelper.asSelfDirtinessTracker; -import static org.hibernate.engine.internal.ManagedTypeHelper.isHibernateProxy; -import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.isSelfDirtinessTracker; import static org.hibernate.engine.internal.ManagedTypeHelper.processIfManagedEntity; import static org.hibernate.engine.internal.ManagedTypeHelper.processIfSelfDirtinessTracker; @@ -56,7 +52,6 @@ import static org.hibernate.engine.spi.Status.SAVING; import static org.hibernate.internal.util.StringHelper.nullIfEmpty; import static org.hibernate.pretty.MessageHelper.infoString; -import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; /** * A base implementation of {@link EntityEntry}. @@ -390,46 +385,31 @@ private boolean isUnequivocallyNonDirty(Object entity) { } private boolean isNonDirtyViaCustomStrategy(Object entity) { - if ( isPersistentAttributeInterceptable( entity ) ) { - final PersistentAttributeInterceptor interceptor = - asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); - if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor ) { + final var interceptable = asPersistentAttributeInterceptableOrNull( entity ); + if ( interceptable != null ) { + if ( interceptable.$$_hibernate_getInterceptor() instanceof EnhancementAsProxyLazinessInterceptor interceptor + && !interceptor.isInitialized() ) { // we never have to check an uninitialized proxy return true; } } - - final SessionImplementor session = (SessionImplementor) getPersistenceContext().getSession(); - final CustomEntityDirtinessStrategy customEntityDirtinessStrategy = - session.getFactory().getCustomEntityDirtinessStrategy(); + final var session = (SessionImplementor) getPersistenceContext().getSession(); + final var customEntityDirtinessStrategy = session.getFactory().getCustomEntityDirtinessStrategy(); return customEntityDirtinessStrategy.canDirtyCheck( entity, persister, session ) && !customEntityDirtinessStrategy.isDirty( entity, persister, session ); } private boolean isNonDirtyViaTracker(Object entity) { - final boolean uninitializedProxy; - if ( isPersistentAttributeInterceptable( entity ) ) { - final PersistentAttributeInterceptor interceptor = - asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); - if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor lazinessInterceptor ) { - return !lazinessInterceptor.hasWrittenFieldNames(); - } - else { - uninitializedProxy = false; + final var interceptable = asPersistentAttributeInterceptableOrNull( entity ); + if ( interceptable != null ) { + if ( interceptable.$$_hibernate_getInterceptor() instanceof EnhancementAsProxyLazinessInterceptor interceptor ) { + return !interceptor.hasWrittenFieldNames(); } } - else if ( isHibernateProxy( entity ) ) { - uninitializedProxy = extractLazyInitializer( entity ).isUninitialized(); - } - else { - uninitializedProxy = false; - } - // we never have to check an uninitialized proxy - return uninitializedProxy - || !persister.hasCollections() - && !persister.hasMutableProperties() - && !asSelfDirtinessTracker( entity ).$$_hibernate_hasDirtyAttributes() - && asManagedEntity( entity ).$$_hibernate_useTracker(); + return !persister.hasCollections() + && !persister.hasMutableProperties() + && asManagedEntity( entity ).$$_hibernate_useTracker() + && !asSelfDirtinessTracker( entity ).$$_hibernate_hasDirtyAttributes(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDirtyCheckEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDirtyCheckEventListener.java index eddb40ab850a..c4f6d8e84c0b 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDirtyCheckEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDirtyCheckEventListener.java @@ -9,14 +9,11 @@ import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.EntityHolder; -import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.Status; import org.hibernate.event.spi.DirtyCheckEvent; import org.hibernate.event.spi.DirtyCheckEventListener; import org.hibernate.event.spi.EventSource; import org.hibernate.persister.collection.CollectionPersister; -import org.hibernate.persister.entity.EntityPersister; - /** * Determines if the current session holds modified state which @@ -38,12 +35,12 @@ public class DefaultDirtyCheckEventListener implements DirtyCheckEventListener { @Override public void onDirtyCheck(DirtyCheckEvent event) throws HibernateException { - final EventSource session = event.getSession(); - final PersistenceContext persistenceContext = session.getPersistenceContext(); + final var session = event.getSession(); + final var persistenceContext = session.getPersistenceContextInternal(); final var holdersByKey = persistenceContext.getEntityHoldersByKey(); if ( holdersByKey != null ) { - for ( var entry : holdersByKey.entrySet() ) { - if ( isEntityDirty( entry.getValue(), session ) ) { + for ( var holder : holdersByKey.values() ) { + if ( isEntityDirty( holder, session ) ) { event.setDirty( true ); return; } @@ -61,25 +58,29 @@ public void onDirtyCheck(DirtyCheckEvent event) throws HibernateException { } private static boolean isEntityDirty(EntityHolder holder, EventSource session) { - final EntityEntry entityEntry = holder.getEntityEntry(); + final var entityEntry = holder.getEntityEntry(); + if ( entityEntry == null ) { + // holders with no entity entry yet cannot contain dirty entities + return false; + } final Status status = entityEntry.getStatus(); return switch ( status ) { case GONE, READ_ONLY -> false; case DELETED -> true; - case MANAGED -> isManagedEntityDirty( holder.getManagedObject(), holder.getDescriptor(), entityEntry, session ); + case MANAGED -> isManagedEntityDirty( holder.getEntity(), entityEntry, session ); case SAVING, LOADING -> throw new AssertionFailure( "Unexpected status: " + status ); }; } - private static boolean isManagedEntityDirty( - Object entity, EntityPersister descriptor, EntityEntry entityEntry, EventSource session) { + private static boolean isManagedEntityDirty(Object entity, EntityEntry entityEntry, EventSource session) { if ( entityEntry.requiresDirtyCheck( entity ) ) { // takes into account CustomEntityDirtinessStrategy - final Object[] propertyValues = + final var persister = entityEntry.getPersister(); + final var propertyValues = entityEntry.getStatus() == Status.DELETED ? entityEntry.getDeletedState() - : descriptor.getValues( entity ); - final int[] dirty = - descriptor.findDirty( propertyValues, entityEntry.getLoadedState(), entity, session ); + : persister.getValues( entity ); + final var dirty = + persister.findDirty( propertyValues, entityEntry.getLoadedState(), entity, session ); return dirty != null; } else { diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index 38cf9f0cd77c..2fdbba2195a1 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -990,8 +990,12 @@ public List findMultiple(Class entityType, List ids, FindOption... @Override public List findMultiple(EntityGraph entityGraph, List ids, FindOption... options) { final RootGraph rootGraph = (RootGraph) entityGraph; + final ManagedDomainType type = rootGraph.getGraphedType(); final MultiIdentifierLoadAccess loadAccess = - byMultipleIds( rootGraph.getGraphedType().getJavaType() ); + switch ( type.getRepresentationMode() ) { + case MAP -> byMultipleIds( type.getTypeName() ); + case POJO -> byMultipleIds( type.getJavaType() ); + }; loadAccess.withLoadGraph( rootGraph ); setMultiIdentifierLoadAccessOptions( options, loadAccess ); return loadAccess.multiLoad( ids ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java index 4244af1dd644..b12c23bfe8e0 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java @@ -690,7 +690,7 @@ protected void firePostRemove(PersistentCollection collection, Object id, Str // collections ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Hibernate Reactive overrides this + // Hibernate Reactive calls this protected void forEachOwnedCollection( Object entity, Object key, EntityPersister persister, BiConsumer> action) { @@ -712,7 +712,7 @@ protected void forEachOwnedCollection( collection = value == null ? instantiateEmpty( key, descriptor ) - : wrap( descriptor, value ); + : wrap( descriptor, value, entity ); } action.accept( descriptor, collection ); } @@ -726,12 +726,12 @@ protected PersistentCollection instantiateEmpty(Object key, CollectionPersist return descriptor.getCollectionSemantics().instantiateWrapper(key, descriptor, this); } - //TODO: is this the right way to do this? - // Hibernate Reactive calls this @SuppressWarnings({"rawtypes", "unchecked"}) - protected PersistentCollection wrap(CollectionPersister descriptor, Object collection) { + protected PersistentCollection wrap(CollectionPersister descriptor, Object collection, Object owner) { final CollectionSemantics collectionSemantics = descriptor.getCollectionSemantics(); - return collectionSemantics.wrap(collection, descriptor, this); + var wrapped = collectionSemantics.wrap( collection, descriptor, this ); + wrapped.setOwner( owner ); + return wrapped; } // loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/collections/LinkedIdentityHashMap.java b/hibernate-core/src/main/java/org/hibernate/internal/util/collections/LinkedIdentityHashMap.java new file mode 100644 index 000000000000..564bf1009e73 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/collections/LinkedIdentityHashMap.java @@ -0,0 +1,297 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.internal.util.collections; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; + + +/** + * Utility {@link Map} implementation that uses identity (==) for key comparison and preserves insertion order + */ +public class LinkedIdentityHashMap extends AbstractMap implements Map { + private static final int DEFAULT_INITIAL_CAPACITY = 16; // must be power of two + + static final class Node implements Map.Entry { + final K key; + V value; + Node next; + Node before; + Node after; + + Node(K key, V value, Node next) { + this.key = key; + this.value = value; + this.next = next; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V newValue) { + final V old = value; + value = newValue; + return old; + } + + @Override + public boolean equals(Object o) { + return o instanceof Node node && key == node.key && Objects.equals( value, node.value ); + } + + @Override + public int hashCode() { + int result = System.identityHashCode( key ); + result = 31 * result + Objects.hashCode( value ); + return result; + } + + @Override + public String toString() { + return key + "=" + value; + } + } + + private Node[] table; + private int size; + + private Node head; + private Node tail; + + private transient Set> entrySet; + + public LinkedIdentityHashMap() { + this( DEFAULT_INITIAL_CAPACITY ); + } + + public LinkedIdentityHashMap(int initialCapacity) { + if ( initialCapacity < 0 ) { + throw new IllegalArgumentException( "Illegal initial capacity: " + initialCapacity ); + } + int cap = 1; + while ( cap < initialCapacity ) { + cap <<= 1; + } + //noinspection unchecked + table = (Node[]) new Node[cap]; + } + + private static int indexFor(int hash, int length) { + return hash & (length - 1); + } + + @Override + public V get(Object key) { + final Node e = getNode( key ); + return e != null ? e.value : null; + } + + private Node getNode(Object key) { + final int hash = System.identityHashCode( key ); + final int idx = indexFor( hash, table.length ); + for ( Node e = table[idx]; e != null; e = e.next ) { + if ( e.key == key ) { + return e; + } + } + return null; + } + + @Override + public boolean containsKey(Object key) { + return getNode( key ) != null; + } + + @Override + public boolean containsValue(Object value) { + for ( Node e = head; e != null; e = e.after ) { + if ( Objects.equals( e.value, value ) ) { + return true; + } + } + return false; + } + + @Override + public V put(K key, V value) { + final int hash = System.identityHashCode( key ); + final int idx = indexFor( hash, table.length ); + for ( Node e = table[idx]; e != null; e = e.next ) { + if ( e.key == key ) { + final V old = e.value; + e.value = value; + return old; + } + } + // not found -> insert + final Node newNode = new Node<>( key, value, table[idx] ); + table[idx] = newNode; + linkLast( newNode ); + size++; + if ( size == table.length ) { + resize(); + } + return null; + } + + private void linkLast(Node node) { + if ( tail == null ) { + head = tail = node; + } + else { + tail.after = node; + node.before = tail; + tail = node; + } + } + + @Override + public V remove(Object key) { + final int hash = System.identityHashCode( key ); + final int idx = indexFor( hash, table.length ); + Node prev = null; + for ( Node e = table[idx]; e != null; prev = e, e = e.next ) { + if ( e.key == key ) { + // remove from bucket chain + if ( prev == null ) { + table[idx] = e.next; + } + else { + prev.next = e.next; + } + // unlink from insertion-order list + final Node b = e.before; + final Node a = e.after; + if ( b == null ) { + head = a; + } + else { + b.after = a; + } + if ( a == null ) { + tail = b; + } + else { + a.before = b; + } + size--; + return e.value; + } + } + return null; + } + + @Override + public void clear() { + Arrays.fill( table, null ); + head = tail = null; + size = 0; + } + + @Override + public int size() { + return size; + } + + private void resize() { + final int oldCap = table.length; + final int newCap = oldCap << 1; + //noinspection unchecked + final Node[] newTable = (Node[]) new Node[newCap]; + for ( int i = 0; i < oldCap; i++ ) { + Node e = table[i]; + while ( e != null ) { + final Node next = e.next; + final int idx = indexFor( System.identityHashCode( e.key ), newCap ); + e.next = newTable[idx]; + newTable[idx] = e; + e = next; + } + } + table = newTable; + } + + final class EntryIterator implements Iterator> { + private Node next = head; + private Node current = null; + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public Node next() { + Node e = next; + if ( e == null ) { + throw new NoSuchElementException(); + } + current = e; + next = e.after; + return e; + } + + @Override + public void remove() { + Node e = current; + if ( e == null ) { + throw new IllegalStateException(); + } + LinkedIdentityHashMap.this.remove( e.key ); + current = null; + } + } + + final class EntrySet extends AbstractSet> { + @Override + public Iterator> iterator() { + return new EntryIterator(); + } + + @Override + public int size() { + return LinkedIdentityHashMap.this.size; + } + + @Override + public void clear() { + LinkedIdentityHashMap.this.clear(); + } + + @Override + public boolean contains(Object o) { + if ( o instanceof Entry e ) { + final Node n = getNode( e.getKey() ); + return n != null && Objects.equals( n.value, e.getValue() ); + } + return false; + } + + @Override + public boolean remove(Object o) { + return o instanceof Entry e && LinkedIdentityHashMap.this.remove( e.getKey() ) != null; + } + } + + @Override + public Set> entrySet() { + Set> es; + return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java index 6b032e3a45ab..4560e81279b5 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java @@ -270,7 +270,7 @@ else if ( configurationValues instanceof Properties properties ) { } /** - * replace a property by a starred version + * Replace a property by a starred version * * @param props properties to check * @param key property to mask @@ -285,6 +285,24 @@ public static Properties maskOut(Properties props, String key) { return clone; } + /** + * Replace properties by starred version + * + * @param props properties to check + * @param keys properties to mask + * + * @return cloned and masked properties + */ + public static Properties maskOut(Properties props, String... keys) { + final Properties clone = (Properties) props.clone(); + for ( String key : keys ) { + if ( clone.get( key ) != null ) { + clone.setProperty( key, "****" ); + } + } + return clone; + } + /** * Extract a property value by name from the given properties object. *

diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java index e9d3d9e0dce0..97d04b32706d 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java @@ -65,6 +65,7 @@ import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; import org.hibernate.type.internal.BasicTypeImpl; +import org.hibernate.type.internal.ConvertedBasicTypeImpl; import org.hibernate.type.spi.TypeConfiguration; import org.hibernate.type.spi.TypeConfigurationAware; import org.hibernate.usertype.DynamicParameterizedType; @@ -849,7 +850,8 @@ public TypeConfiguration getTypeConfiguration() { // return EnumeratedValueResolution.fromName( name, stdIndicators, context ); // } - if ( name.startsWith( BasicTypeImpl.EXTERNALIZED_PREFIX ) ) { + if ( name.startsWith( BasicTypeImpl.EXTERNALIZED_PREFIX ) + || name.startsWith( ConvertedBasicTypeImpl.EXTERNALIZED_PREFIX ) ) { return getNamedBasicTypeResolution( bootstrapContext.resolveAdHocBasicType( name ), explicitMutabilityPlanAccess, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractEntityInstantiatorPojo.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractEntityInstantiatorPojo.java index ed33ffb83280..3f3329580570 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractEntityInstantiatorPojo.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractEntityInstantiatorPojo.java @@ -66,4 +66,18 @@ public boolean isInstance(Object object) { // this one needed only for guessEntityMode() || proxyInterface!=null && proxyInterface.isInstance(object); } + + /* + * Used by Hibernate Reactive + */ + protected boolean isApplyBytecodeInterception() { + return applyBytecodeInterception; + } + + /* + * Used by Hibernate Reactive + */ + protected LazyAttributeLoadingInterceptor.EntityRelatedState getLoadingInterceptorState() { + return loadingInterceptorState; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java index 0284d4137c5e..719d616b588c 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java @@ -163,7 +163,10 @@ private Map buildPropertyAccessMap(PersistentClass bootD return propertyAccessMap; } - private EntityInstantiator determineInstantiator(PersistentClass bootDescriptor, EntityMetamodel entityMetamodel) { + /* + * Used by Hibernate Reactive + */ + protected EntityInstantiator determineInstantiator(PersistentClass bootDescriptor, EntityMetamodel entityMetamodel) { if ( reflectionOptimizer != null && reflectionOptimizer.getInstantiationOptimizer() != null ) { return new EntityInstantiatorPojoOptimized( entityMetamodel, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEmbeddableMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEmbeddableMapping.java index 112cbd25fc66..82bda15f6147 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEmbeddableMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEmbeddableMapping.java @@ -241,6 +241,7 @@ else if ( attributeMapping instanceof ToOneAttributeMapping original ) { creationProcess ) ); + toOne.setupCircularFetchModelPart( creationProcess ); attributeMapping = toOne; currentIndex += attributeMapping.getJdbcTypeCount(); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java index 18862ede8e60..b20c539a4997 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java @@ -877,7 +877,8 @@ public static boolean interpretToOneKeyDescriptor( return interpretNestedToOneKeyDescriptor( referencedEntityDescriptor, referencedPropertyName, - attributeMapping + attributeMapping, + creationProcess ); } @@ -909,6 +910,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa creationProcess ); attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); } else { throw new UnsupportedOperationException( @@ -997,6 +999,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa swapDirection ); attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); creationProcess.registerForeignKey( attributeMapping, foreignKeyDescriptor ); } else if ( fkTarget instanceof EmbeddableValuedModelPart embeddableValuedModelPart ) { @@ -1013,6 +1016,7 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart embeddableValuedModelPar creationProcess ); attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); creationProcess.registerForeignKey( attributeMapping, embeddedForeignKeyDescriptor ); } else { @@ -1033,12 +1037,14 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart embeddableValuedModelPar * @param referencedEntityDescriptor The entity which contains the inverse property * @param referencedPropertyName The inverse property name path * @param attributeMapping The attribute for which we try to set the foreign key + * @param creationProcess The creation process * @return true if the foreign key is actually set */ private static boolean interpretNestedToOneKeyDescriptor( EntityPersister referencedEntityDescriptor, String referencedPropertyName, - ToOneAttributeMapping attributeMapping) { + ToOneAttributeMapping attributeMapping, + MappingModelCreationProcess creationProcess) { final String[] propertyPath = split( ".", referencedPropertyName ); EmbeddableValuedModelPart lastEmbeddableModelPart = null; @@ -1058,6 +1064,7 @@ else if ( modelPart instanceof ToOneAttributeMapping referencedAttributeMapping } else { attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); return true; } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java index f62c7577e973..0b51e85a1e3e 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java @@ -31,6 +31,7 @@ import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.AttributeMetadata; import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.EntityAssociationMapping; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; @@ -172,6 +173,7 @@ public class Entity1 { private ForeignKeyDescriptor.Nature sideNature; private String identifyingColumnsTableExpression; private boolean canUseParentTableGroup; + private @Nullable EmbeddableValuedModelPart circularFetchModelPart; /** * For Hibernate Reactive @@ -841,6 +843,27 @@ public void setForeignKeyDescriptor(ForeignKeyDescriptor foreignKeyDescriptor) { && declaringTableGroupProducer.containsTableReference( identifyingColumnsTableExpression ); } + public void setupCircularFetchModelPart(MappingModelCreationProcess creationProcess) { + if ( sideNature == ForeignKeyDescriptor.Nature.TARGET + && getAssociatedEntityMappingType().getIdentifierMapping() instanceof CompositeIdentifierMapping identifierMapping + && foreignKeyDescriptor.getKeyPart() != identifierMapping ) { + // Setup a special embeddable model part for fetching the key object for a circular fetch. + // This is needed if the association entity nests the "inverse" toOne association in the embedded id, + // because then, the key part of the foreign key is just a simple value instead of the expected embedded id + // when doing delayed creation/querying of target entities. See HHH-19687 for details + this.circularFetchModelPart = MappingModelCreationHelper.createInverseModelPart( + identifierMapping, + getDeclaringType(), + this, + foreignKeyDescriptor.getTargetPart(), + creationProcess + ); + } + else { + this.circularFetchModelPart = null; + } + } + public String getIdentifyingColumnsTableExpression() { return identifyingColumnsTableExpression; } @@ -1024,34 +1047,6 @@ class Mother { We have a circularity but it is not bidirectional */ - final TableGroup parentTableGroup = creationState - .getSqlAstCreationState() - .getFromClauseAccess() - .getTableGroup( fetchParent.getNavigablePath() ); - final DomainResult foreignKeyDomainResult; - assert !creationState.isResolvingCircularFetch(); - try { - creationState.setResolvingCircularFetch( true ); - if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { - foreignKeyDomainResult = foreignKeyDescriptor.createKeyDomainResult( - fetchablePath, - createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ), - fetchParent, - creationState - ); - } - else { - foreignKeyDomainResult = foreignKeyDescriptor.createTargetDomainResult( - fetchablePath, - parentTableGroup, - fetchParent, - creationState - ); - } - } - finally { - creationState.setResolvingCircularFetch( false ); - } return new CircularFetchImpl( this, fetchTiming, @@ -1059,13 +1054,52 @@ class Mother { fetchParent, isSelectByUniqueKey( sideNature ), parentNavigablePath, - foreignKeyDomainResult, + determineCircularKeyResult( fetchParent, fetchablePath, creationState ), creationState ); } return null; } + private DomainResult determineCircularKeyResult( + FetchParent fetchParent, + NavigablePath fetchablePath, + DomainResultCreationState creationState) { + final FromClauseAccess fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess(); + final TableGroup parentTableGroup = fromClauseAccess.getTableGroup( fetchParent.getNavigablePath() ); + assert !creationState.isResolvingCircularFetch(); + try { + creationState.setResolvingCircularFetch( true ); + if ( circularFetchModelPart != null ) { + return circularFetchModelPart.createDomainResult( + fetchablePath, + createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ), + null, + creationState + ); + } + else if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { + return foreignKeyDescriptor.createKeyDomainResult( + fetchablePath, + createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ), + fetchParent, + creationState + ); + } + else { + return foreignKeyDescriptor.createTargetDomainResult( + fetchablePath, + parentTableGroup, + fetchParent, + creationState + ); + } + } + finally { + creationState.setResolvingCircularFetch( false ); + } + } + protected boolean isBidirectionalAttributeName( NavigablePath parentNavigablePath, ModelPart parentModelPart, diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index 3c753c1f28ea..3217e9650e34 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -134,7 +134,6 @@ import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.ManagedMappingType; -import org.hibernate.metamodel.mapping.internal.MappingModelHelper; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.NaturalIdMapping; import org.hibernate.metamodel.mapping.NonAggregatedIdentifierMapping; @@ -304,6 +303,7 @@ import static org.hibernate.internal.util.collections.CollectionHelper.toSmallList; import static org.hibernate.loader.ast.internal.MultiKeyLoadHelper.supportsSqlArrayType; import static org.hibernate.metamodel.RepresentationMode.POJO; +import static org.hibernate.metamodel.mapping.internal.MappingModelHelper.isCompatibleModelPart; import static org.hibernate.persister.entity.DiscriminatorHelper.NOT_NULL_DISCRIMINATOR; import static org.hibernate.persister.entity.DiscriminatorHelper.NULL_DISCRIMINATOR; import static org.hibernate.pretty.MessageHelper.infoString; @@ -1876,62 +1876,62 @@ public String selectFragment(String alias, String suffix) { // Wrap expressions with aliases final SelectClause selectClause = rootQuerySpec.getSelectClause(); final List sqlSelections = selectClause.getSqlSelections(); + final Set processedExpressions = new HashSet<>( sqlSelections.size() ); int i = 0; - int columnIndex = 0; - final String[] columnAliases = getSubclassColumnAliasClosure(); - final int columnAliasesSize = columnAliases.length; - for ( String identifierAlias : identifierAliases ) { - sqlSelections.set( - i, - new SqlSelectionImpl( - i, - new AliasedExpression( sqlSelections.get( i ).getExpression(), identifierAlias + suffix ) - ) - ); - if ( i < columnAliasesSize && columnAliases[i].equals( identifierAlias ) ) { - columnIndex++; + final int identifierSelectionSize = identifierMapping.getJdbcTypeCount(); + for ( int j = 0; j < identifierSelectionSize; j++ ) { + final SelectableMapping selectableMapping = identifierMapping.getSelectable( j ); + if ( processedExpressions.add( selectableMapping.getSelectionExpression() ) ) { + aliasSelection( sqlSelections, i, identifierAliases[j] + suffix ); + i++; } - i++; } - if ( entityMetamodel.hasSubclasses() ) { - sqlSelections.set( - i, - new SqlSelectionImpl( - i, - new AliasedExpression( sqlSelections.get( i ).getExpression(), getDiscriminatorAlias() + suffix ) - ) - ); - i++; + if ( hasSubclasses() ) { + assert discriminatorMapping.getJdbcTypeCount() == 1; + final SelectableMapping selectableMapping = discriminatorMapping.getSelectable( 0 ); + if ( processedExpressions.add( selectableMapping.getSelectionExpression() ) ) { + aliasSelection( sqlSelections, i, getDiscriminatorAlias() + suffix ); + i++; + } } if ( hasRowId() ) { - sqlSelections.set( - i, - new SqlSelectionImpl( - i, - new AliasedExpression( sqlSelections.get( i ).getExpression(), ROWID_ALIAS + suffix ) - ) - ); - i++; + final SelectableMapping selectableMapping = rowIdMapping; + if ( processedExpressions.add( selectableMapping.getSelectionExpression() ) ) { + aliasSelection( sqlSelections, i, ROWID_ALIAS + suffix ); + i++; + } } + final String[] columnAliases = getSubclassColumnAliasClosure(); final String[] formulaAliases = getSubclassFormulaAliasClosure(); + int columnIndex = 0; int formulaIndex = 0; - for ( ; i < sqlSelections.size(); i++ ) { - final SqlSelection sqlSelection = sqlSelections.get( i ); - final ColumnReference columnReference = (ColumnReference) sqlSelection.getExpression(); - final String selectAlias = - columnReference.isColumnExpressionFormula() - ? formulaAliases[formulaIndex++] + suffix - : columnAliases[columnIndex++] + suffix; - sqlSelections.set( - i, - new SqlSelectionImpl( - sqlSelection.getValuesArrayPosition(), - new AliasedExpression( sqlSelection.getExpression(), selectAlias ) - ) - ); + final int size = getNumberOfFetchables(); + // getSubclassColumnAliasClosure contains the _identifierMapper columns when it has an id class, + // which need to be skipped + if ( identifierMapping instanceof NonAggregatedIdentifierMapping nonAggregatedIdentifierMapping + && nonAggregatedIdentifierMapping.getIdClassEmbeddable() != null ) { + columnIndex = identifierSelectionSize; + } + for ( int j = 0; j < size; j++ ) { + final AttributeMapping fetchable = getFetchable( j ); + if ( !(fetchable instanceof PluralAttributeMapping) + && !skipFetchable( fetchable, fetchable.getMappedFetchOptions().getTiming() ) + && fetchable.isSelectable() ) { + final int jdbcTypeCount = fetchable.getJdbcTypeCount(); + for ( int k = 0; k < jdbcTypeCount; k++ ) { + final SelectableMapping selectableMapping = fetchable.getSelectable( k ); + if ( processedExpressions.add( selectableMapping.getSelectionExpression() ) ) { + final String baseAlias = selectableMapping.isFormula() + ? formulaAliases[formulaIndex++] + : columnAliases[columnIndex++]; + aliasSelection( sqlSelections, i, baseAlias + suffix ); + i++; + } + } + } } final String sql = @@ -1945,6 +1945,17 @@ public String selectFragment(String alias, String suffix) { : sql.substring( "select ".length() ); } + private static void aliasSelection( + List sqlSelections, + int selectionIndex, + String alias) { + final Expression expression = sqlSelections.get( selectionIndex ).getExpression(); + sqlSelections.set( + selectionIndex, + new SqlSelectionImpl( selectionIndex, new AliasedExpression( expression, alias ) ) + ); + } + private ImmutableFetchList fetchProcessor(FetchParent fetchParent, LoaderSqlAstCreationState creationState) { final FetchableContainer fetchableContainer = fetchParent.getReferencedMappingContainer(); final int size = fetchableContainer.getNumberOfFetchables(); @@ -5762,50 +5773,30 @@ public ModelPart findSubPart(String name, EntityMappingType treatTargetType) { } } - if ( treatTargetType != null ) { - if ( ! treatTargetType.isTypeOrSuperType( this ) ) { + if ( treatTargetType == null ) { + final var subDefinedAttribute = findSubPartInSubclassMappings( name ); + if ( subDefinedAttribute != null ) { + return subDefinedAttribute; + } + } + else if ( treatTargetType != this ) { + if ( !treatTargetType.isTypeOrSuperType( this ) ) { return null; } - - if ( subclassMappingTypes != null && !subclassMappingTypes.isEmpty() ) { - for ( EntityMappingType subMappingType : subclassMappingTypes.values() ) { - if ( ! treatTargetType.isTypeOrSuperType( subMappingType ) ) { - continue; - } - - final ModelPart subDefinedAttribute = subMappingType.findSubTypesSubPart( name, treatTargetType ); - - if ( subDefinedAttribute != null ) { - return subDefinedAttribute; - } - } + // Prefer attributes defined in the treat target type or its subtypes + final var treatTypeSubPart = treatTargetType.findSubTypesSubPart( name, null ); + if ( treatTypeSubPart != null ) { + return treatTypeSubPart; } - } - else { - if ( subclassMappingTypes != null && !subclassMappingTypes.isEmpty() ) { - ModelPart attribute = null; - for ( EntityMappingType subMappingType : subclassMappingTypes.values() ) { - final ModelPart subDefinedAttribute = subMappingType.findSubTypesSubPart( name, treatTargetType ); - if ( subDefinedAttribute != null ) { - if ( attribute != null && !MappingModelHelper.isCompatibleModelPart( attribute, subDefinedAttribute ) ) { - throw new PathException( - String.format( - Locale.ROOT, - "Could not resolve attribute '%s' of '%s' due to the attribute being declared in multiple subtypes '%s' and '%s'", - name, - getJavaType().getTypeName(), - attribute.asAttributeMapping().getDeclaringType() - .getJavaType().getTypeName(), - subDefinedAttribute.asAttributeMapping().getDeclaringType() - .getJavaType().getTypeName() - ) - ); - } - attribute = subDefinedAttribute; + else { + // If not found, look in the treat target type's supertypes + EntityMappingType superType = treatTargetType.getSuperMappingType(); + while ( superType != this ) { + final var superTypeSubPart = superType.findDeclaredAttributeMapping( name ); + if ( superTypeSubPart != null ) { + return superTypeSubPart; } - } - if ( attribute != null ) { - return attribute; + superType = superType.getSuperMappingType(); } } } @@ -5827,6 +5818,29 @@ public ModelPart findSubPart(String name, EntityMappingType treatTargetType) { } } + private ModelPart findSubPartInSubclassMappings(String name) { + ModelPart attribute = null; + if ( isNotEmpty( subclassMappingTypes ) ) { + for ( var subMappingType : subclassMappingTypes.values() ) { + final var subDefinedAttribute = subMappingType.findSubTypesSubPart( name, null ); + if ( subDefinedAttribute != null ) { + if ( attribute != null && !isCompatibleModelPart( attribute, subDefinedAttribute ) ) { + throw new PathException( String.format( + Locale.ROOT, + "Could not resolve attribute '%s' of '%s' due to the attribute being declared in multiple subtypes '%s' and '%s'", + name, + getJavaType().getTypeName(), + attribute.asAttributeMapping().getDeclaringType().getJavaType().getTypeName(), + subDefinedAttribute.asAttributeMapping().getDeclaringType().getJavaType().getTypeName() + ) ); + } + attribute = subDefinedAttribute; + } + } + } + return attribute; + } + @Override public ModelPart findSubTypesSubPart(String name, EntityMappingType treatTargetType) { final AttributeMapping declaredAttribute = declaredAttributeMappings.get( name ); @@ -5834,15 +5848,7 @@ public ModelPart findSubTypesSubPart(String name, EntityMappingType treatTargetT return declaredAttribute; } else { - if ( subclassMappingTypes != null && !subclassMappingTypes.isEmpty() ) { - for ( EntityMappingType subMappingType : subclassMappingTypes.values() ) { - final ModelPart subDefinedAttribute = subMappingType.findSubTypesSubPart( name, treatTargetType ); - if ( subDefinedAttribute != null ) { - return subDefinedAttribute; - } - } - } - return null; + return findSubPartInSubclassMappings( name ); } } @@ -5902,7 +5908,7 @@ public Fetchable getKeyFetchable(int position) { } @Override - public Fetchable getFetchable(int position) { + public AttributeMapping getFetchable(int position) { return getStaticFetchableList().get( position ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java index d09b48affd62..bfe15832b68d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java @@ -207,6 +207,10 @@ else if ( fetch ) { } } } + if ( !(subPathSource instanceof SqmJoinable) ) { + throw new SemanticException( "Joining on basic value elements is not supported", + ((SemanticQueryBuilder) creationState).getQuery() ); + } @SuppressWarnings("unchecked") final SqmJoinable joinSource = (SqmJoinable) subPathSource; return createJoin( lhs, joinType, alias, fetch, isTerminal, allowReuse, creationState, joinSource ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPredicatePathConsumer.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPredicatePathConsumer.java index 784aac84341d..724d8639e332 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPredicatePathConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPredicatePathConsumer.java @@ -4,6 +4,7 @@ */ package org.hibernate.query.hql.internal; +import java.util.List; import java.util.Locale; import org.hibernate.query.SemanticException; @@ -56,9 +57,9 @@ protected void validateAsRoot(SqmFrom pathRoot) { final SqmFromClause fromClause = querySpec.getFromClause(); // If the current processing query contains the root of the current join, // then the root of the processing path must be a root of one of the parent queries - if ( fromClause != null && fromClause.getRoots().contains( joinRoot ) ) { + if ( fromClause != null && contains( fromClause.getRoots(), joinRoot ) ) { // It is allowed to use correlations from the same query - if ( !( root instanceof SqmCorrelation ) || !fromClause.getRoots().contains( root ) ) { + if ( !( root instanceof SqmCorrelation ) || !contains( fromClause.getRoots(), root ) ) { validateAsRootOnParentQueryClosure( pathRoot, root, processingState.getParentProcessingState() ); } @@ -97,7 +98,7 @@ private void validateAsRootOnParentQueryClosure( // If we are in a subquery, the "foreign" from element could be one of the subquery roots, // which is totally fine. The aim of this check is to prevent uses of different "spaces" // i.e. `from A a, B b join b.id = a.id` would be illegal - if ( fromClause != null && fromClause.getRoots().contains( root ) ) { + if ( fromClause != null && contains( fromClause.getRoots(), root ) ) { super.validateAsRoot( pathRoot ); return; } @@ -113,6 +114,15 @@ private void validateAsRootOnParentQueryClosure( ) ); } + + private boolean contains(List> roots, SqmRoot root) { + for ( SqmRoot sqmRoot : roots ) { + if ( sqmRoot == root ) { + return true; + } + } + return false; + } }; } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index be7845bc1ab4..0a528c0a7418 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -414,6 +414,10 @@ public Stack getProcessingStateStack() { return processingStateStack; } + public String getQuery() { + return query; + } + private NodeBuilder nodeBuilder() { return creationContext.getNodeBuilder(); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/MutationSpecificationImpl.java b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/MutationSpecificationImpl.java index 64a01b53095e..edccc12cf4d8 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/MutationSpecificationImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/MutationSpecificationImpl.java @@ -41,6 +41,7 @@ import java.util.function.BiConsumer; import static org.hibernate.query.sqm.tree.SqmCopyContext.noParamCopyContext; +import static org.hibernate.query.sqm.tree.SqmCopyContext.simpleContext; /** * Standard implementation of MutationSpecification @@ -124,7 +125,7 @@ public MutationQuery createQuery(StatelessSession session) { public MutationQuery createQuery(SharedSessionContract session) { final var sessionImpl = session.unwrap(SharedSessionContractImplementor.class); final SqmDeleteOrUpdateStatement sqmStatement = build( sessionImpl.getFactory().getQueryEngine() ); - return new SqmQueryImpl<>( sqmStatement, true, null, sessionImpl ); + return new SqmQueryImpl<>( sqmStatement, false, null, sessionImpl ); } private SqmDeleteOrUpdateStatement build(QueryEngine queryEngine) { @@ -135,7 +136,8 @@ private SqmDeleteOrUpdateStatement build(QueryEngine queryEngine) { mutationTargetRoot = resolveSqmRoot( sqmStatement, mutationTarget ); } else if ( deleteOrUpdateStatement != null ) { - sqmStatement = deleteOrUpdateStatement; + sqmStatement = (SqmDeleteOrUpdateStatement) deleteOrUpdateStatement + .copy( simpleContext() ); mutationTargetRoot = resolveSqmRoot( sqmStatement, sqmStatement.getTarget().getManagedType().getJavaType() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SelectionSpecificationImpl.java b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SelectionSpecificationImpl.java index afd15e366390..72adfc39f1f7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SelectionSpecificationImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SelectionSpecificationImpl.java @@ -46,6 +46,7 @@ import static org.hibernate.internal.util.collections.CollectionHelper.isEmpty; import static org.hibernate.query.sqm.internal.SqmUtil.validateCriteriaQuery; import static org.hibernate.query.sqm.tree.SqmCopyContext.noParamCopyContext; +import static org.hibernate.query.sqm.tree.SqmCopyContext.simpleContext; /** * Standard implementation of SelectionSpecification @@ -166,7 +167,7 @@ public SelectionQuery createQuery(StatelessSession session) { public SelectionQuery createQuery(SharedSessionContract session) { final var sessionImpl = session.unwrap(SharedSessionContractImplementor.class); final SqmSelectStatement sqmStatement = build( sessionImpl.getFactory().getQueryEngine() ); - return new SqmSelectionQueryImpl<>( sqmStatement, true, resultType, sessionImpl ); + return new SqmSelectionQueryImpl<>( sqmStatement, false, resultType, sessionImpl ); } private SqmSelectStatement build(QueryEngine queryEngine) { @@ -177,7 +178,7 @@ private SqmSelectStatement build(QueryEngine queryEngine) { sqmRoot = extractRoot( sqmStatement, resultType, hql ); } else if ( criteriaQuery != null ) { - sqmStatement = (SqmSelectStatement) criteriaQuery; + sqmStatement = ((SqmSelectStatement) criteriaQuery).copy( simpleContext() ); sqmRoot = extractRoot( sqmStatement, resultType, "criteria query" ); } else { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmAggregateFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmAggregateFunction.java index 724a93b69823..366bc0e1c5b7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmAggregateFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmAggregateFunction.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.hibernate.metamodel.model.domain.ReturnableType; import org.hibernate.query.sqm.NodeBuilder; @@ -123,4 +124,24 @@ public void appendHqlString(StringBuilder hql, SqmRenderContext context) { hql.append( ')' ); } } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + if ( !super.equals( o ) ) { + return false; + } + + SelfRenderingSqmAggregateFunction that = (SelfRenderingSqmAggregateFunction) o; + return Objects.equals( filter, that.filter ); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Objects.hashCode( filter ); + return result; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmFunction.java index 58e35b583a2a..ea201483a9c3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmFunction.java @@ -6,7 +6,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.function.Supplier; import org.hibernate.metamodel.mapping.BasicValuedMapping; @@ -255,16 +254,4 @@ public MappingModelExpressible get() { return argumentTypeResolver.resolveFunctionArgumentType( function.getArguments(), argumentIndex, converter ); } } - - @Override - // TODO: override on all subtypes - public boolean equals(Object other) { - return other instanceof SelfRenderingSqmAggregateFunction that - && Objects.equals( this.toHqlString(), that.toHqlString() ); - } - - @Override - public int hashCode() { - return toHqlString().hashCode(); - } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmOrderedSetAggregateFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmOrderedSetAggregateFunction.java index 8125588036a5..a32b1f3c944e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmOrderedSetAggregateFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmOrderedSetAggregateFunction.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import org.hibernate.metamodel.model.domain.ReturnableType; import org.hibernate.query.sqm.NodeBuilder; @@ -173,4 +174,24 @@ public void appendHqlString(StringBuilder hql, SqmRenderContext context) { hql.append( ')' ); } } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + if ( !super.equals( o ) ) { + return false; + } + + SelfRenderingSqmOrderedSetAggregateFunction that = (SelfRenderingSqmOrderedSetAggregateFunction) o; + return Objects.equals( withinGroup, that.withinGroup ); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Objects.hashCode( withinGroup ); + return result; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmWindowFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmWindowFunction.java index a9b474046fdc..04dc8bdc62af 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmWindowFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmWindowFunction.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.hibernate.metamodel.model.domain.ReturnableType; import org.hibernate.query.sqm.NodeBuilder; @@ -121,7 +122,7 @@ public void appendHqlString(StringBuilder hql, SqmRenderContext context) { hql.append( getFunctionName() ); hql.append( '(' ); int i = 1; - if ( arguments.get( 0 ) instanceof SqmDistinct ) { + if ( !arguments.isEmpty() && arguments.get( 0 ) instanceof SqmDistinct ) { arguments.get( 0 ).appendHqlString( hql, context ); if ( arguments.size() > 1 ) { hql.append( ' ' ); @@ -157,4 +158,28 @@ public void appendHqlString(StringBuilder hql, SqmRenderContext context) { hql.append( ')' ); } } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + if ( !super.equals( o ) ) { + return false; + } + + SelfRenderingSqmWindowFunction that = (SelfRenderingSqmWindowFunction) o; + return Objects.equals( filter, that.filter ) + && Objects.equals( respectNulls, that.respectNulls ) + && Objects.equals( fromFirst, that.fromFirst ); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Objects.hashCode( filter ); + result = 31 * result + Objects.hashCode( respectNulls ); + result = 31 * result + Objects.hashCode( fromFirst ); + return result; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java index f4da8f36a638..b4fb034756e6 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java @@ -8,6 +8,7 @@ import org.hibernate.HibernateException; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.graph.spi.AppliedGraph; +import org.hibernate.query.sqm.tree.expression.ValueBindJpaCriteriaParameter; import org.hibernate.type.BindableType; import org.hibernate.query.IllegalSelectQueryException; import org.hibernate.query.KeyedPage; @@ -158,14 +159,11 @@ else if ( bindType != null ) { protected void bindCriteriaParameter(SqmJpaCriteriaParameterWrapper sqmParameter) { final JpaCriteriaParameter criteriaParameter = sqmParameter.getJpaCriteriaParameter(); - final T value = criteriaParameter.getValue(); - // We don't set a null value, unless the type is also null which - // is the case when using HibernateCriteriaBuilder.value - if ( value != null || criteriaParameter.getNodeType() == null ) { + if ( criteriaParameter instanceof ValueBindJpaCriteriaParameter ) { // Use the anticipated type for binding the value if possible getQueryParameterBindings() .getBinding( criteriaParameter ) - .setBindValue( value, criteriaParameter.getAnticipatedType() ); + .setBindValue( criteriaParameter.getValue(), criteriaParameter.getAnticipatedType() ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/DomainParameterXref.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/DomainParameterXref.java index dc5c8a1868d9..79d7e2929f10 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/DomainParameterXref.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/DomainParameterXref.java @@ -6,11 +6,11 @@ import java.util.ArrayList; import java.util.IdentityHashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; +import org.hibernate.internal.util.collections.LinkedIdentityHashMap; import org.hibernate.query.internal.QueryParameterNamedImpl; import org.hibernate.query.internal.QueryParameterPositionalImpl; import org.hibernate.query.spi.QueryParameterImplementor; @@ -31,7 +31,7 @@ public class DomainParameterXref { public static final DomainParameterXref EMPTY = new DomainParameterXref( - new LinkedHashMap<>( 0 ), + new LinkedIdentityHashMap<>( 0 ), new IdentityHashMap<>( 0 ), SqmStatement.ParameterResolutions.empty() ); @@ -46,8 +46,8 @@ public static DomainParameterXref from(SqmStatement sqmStatement) { } else { final int sqmParamCount = parameterResolutions.getSqmParameters().size(); - final LinkedHashMap, List>> sqmParamsByQueryParam = - new LinkedHashMap<>( sqmParamCount ); + final Map, List>> sqmParamsByQueryParam = + new LinkedIdentityHashMap<>( sqmParamCount ); final IdentityHashMap, QueryParameterImplementor> queryParamBySqmParam = new IdentityHashMap<>( sqmParamCount ); @@ -118,13 +118,13 @@ else if ( sqmParameter.getExpressible() != null private final SqmStatement.ParameterResolutions parameterResolutions; - private final LinkedHashMap, List>> sqmParamsByQueryParam; + private final Map, List>> sqmParamsByQueryParam; private final IdentityHashMap, QueryParameterImplementor> queryParamBySqmParam; private Map,List>> expansions; private DomainParameterXref( - LinkedHashMap, List>> sqmParamsByQueryParam, + Map, List>> sqmParamsByQueryParam, IdentityHashMap, QueryParameterImplementor> queryParamBySqmParam, SqmStatement.ParameterResolutions parameterResolutions) { this.sqmParamsByQueryParam = sqmParamsByQueryParam; @@ -148,7 +148,9 @@ public boolean hasParameters() { } /** - * Get all of the QueryParameters mapped by this xref + * Get all the QueryParameters mapped by this xref. + * Note that order of parameters is important - parameters are + * included in cache keys for query results caching. */ public Map, List>> getQueryParameters() { return sqmParamsByQueryParam; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 070e2d5a0874..b9318b1ad0cb 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -2147,7 +2147,10 @@ else if ( value instanceof SqmExpression ) { } private boolean isInstance(BindableType bindableType, T value) { - if ( bindableType instanceof SqmExpressible expressible ) { + if ( value == null ) { + return true; + } + else if ( bindableType instanceof SqmExpressible expressible ) { return expressible.getExpressibleJavaType().isInstance( value ); } else { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index 7213d5d2fe44..51d829a99980 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -6199,12 +6199,12 @@ private void resolveSqmParameter( BiConsumer jdbcParameterConsumer) { sqmParameterMappingModelTypes.put( expression, valueMapping ); final List> jdbcParams = jdbcParamsBySqmParam.get( expression ); - final int parameterId = jdbcParams == null ? jdbcParamsBySqmParam.size() + final int parameterId = jdbcParams == null ? jdbcParameters.getJdbcParameters().size() : NullnessUtil.castNonNull( jdbcParams.get( 0 ).get( 0 ).getParameterId() ); final Bindable bindable = bindable( valueMapping ); if ( bindable instanceof SelectableMappings selectableMappings ) { selectableMappings.forEachSelectable( - (index, selectableMapping) -> jdbcParameterConsumer.accept( index, new SqlTypedMappingJdbcParameter( selectableMapping, parameterId ) ) + (index, selectableMapping) -> jdbcParameterConsumer.accept( index, new SqlTypedMappingJdbcParameter( selectableMapping, parameterId + index ) ) ); } else if ( bindable instanceof SelectableMapping selectableMapping ) { @@ -6220,7 +6220,7 @@ else if ( bindable instanceof SelectableMapping selectableMapping ) { bindable.forEachJdbcType( (index, jdbcMapping) -> jdbcParameterConsumer.accept( index, - new JdbcParameterImpl( jdbcMapping, parameterId ) + new JdbcParameterImpl( jdbcMapping, parameterId + index ) ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmFunction.java index affda085cafd..3f2a4fc1599f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmFunction.java @@ -206,8 +206,15 @@ public SqmPath resolveIndexedAccess( @Override public boolean equals(Object other) { - return other instanceof SqmFunction that - && Objects.equals( this.functionName, that.functionName ) + if ( this == other ) { + return true; + } + if ( other == null || getClass() != other.getClass() ) { + return false; + } + + final SqmFunction that = (SqmFunction) other; + return Objects.equals( this.functionName, that.functionName ) && Objects.equals( this.arguments, that.arguments ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/ValueBindJpaCriteriaParameter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/ValueBindJpaCriteriaParameter.java index 950551b4ee18..880520028197 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/ValueBindJpaCriteriaParameter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/ValueBindJpaCriteriaParameter.java @@ -55,33 +55,39 @@ public void appendHqlString(StringBuilder hql, SqmRenderContext context) { } @Override - // TODO: fix this public int compareTo(SqmParameter parameter) { - return this == parameter ? 0 : 1; + return Integer.compare( hashCode(), parameter.hashCode() ); } - // this is not really a parameter, it's really a literal value - // so use value equality based on its value - @Override - public boolean equals(Object object) { - return object instanceof ValueBindJpaCriteriaParameter that - && Objects.equals( this.value, that.value ); -// && getJavaTypeDescriptor().areEqual( this.value, (T) that.value ); + public boolean equals(Object obj) { + if ( this == obj ) { + return true; + } + if ( obj instanceof ValueBindJpaCriteriaParameter that ) { + if ( value == null ) { + return that.value == null && Objects.equals( getNodeType(), that.getNodeType() ); + } + final var javaType = getJavaTypeDescriptor(); + if ( that.value != null ) { + if ( javaType != null ) { + //noinspection unchecked + return javaType.equals( that.getJavaTypeDescriptor() ) && javaType.areEqual( value, (T) that.value ); + } + else { + return that.getJavaTypeDescriptor() == null && value.equals( that.value ); + } + } + } + return false; } @Override public int hashCode() { - return value == null ? 0 : value.hashCode(); // getJavaTypeDescriptor().extractHashCode( value ); + if ( value == null ) { + return 0; + } + final var javaType = getJavaTypeDescriptor(); + return javaType == null ? value.hashCode() : javaType.extractHashCode( value ); } - -// @Override -// public boolean equals(Object object) { -// return this == object; -// } -// -// @Override -// public int hashCode() { -// return System.identityHashCode( this ); -// } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/jpa/ParameterCollector.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/jpa/ParameterCollector.java index ea302c20026a..633e5f2451a2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/jpa/ParameterCollector.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/jpa/ParameterCollector.java @@ -5,7 +5,7 @@ package org.hibernate.query.sqm.tree.jpa; import java.util.Collections; -import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.Set; import java.util.function.Consumer; @@ -132,7 +132,7 @@ private BindableType getInferredParameterType(JpaCriteriaParameter exp private > T visitParameter(T param) { if ( parameterExpressions == null ) { - parameterExpressions = new HashSet<>(); + parameterExpressions = Collections.newSetFromMap( new IdentityHashMap<>() ); } parameterExpressions.add( param ); consumer.accept( param ); @@ -141,7 +141,7 @@ private > T visitParameter(T param) { private SqmJpaCriteriaParameterWrapper visitParameter(SqmJpaCriteriaParameterWrapper param) { if ( parameterExpressions == null ) { - parameterExpressions = new HashSet<>(); + parameterExpressions = Collections.newSetFromMap( new IdentityHashMap<>() ); } parameterExpressions.add( param.getJpaCriteriaParameter() ); consumer.accept( param ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java index a6f2fbf6dd00..3806609fff1a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; import java.util.Set; @@ -516,7 +517,7 @@ public void validateFetchOwners() { } } else { - selectedFromSet = new HashSet<>( selectClause.getSelections().size() ); + selectedFromSet = Collections.newSetFromMap( new IdentityHashMap<>( selectClause.getSelections().size() ) ); for ( SqmSelection selection : selectClause.getSelections() ) { collectSelectedFromSet( selectedFromSet, selection.getSelectableNode() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java index c47f330a5870..012adb930a2b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java @@ -7,7 +7,8 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.Collection; -import java.util.HashSet; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -580,7 +581,7 @@ public JpaEntityJoin correlate(JpaEntityJoin parentEntityJoin) { @Override public Set> getCorrelatedJoins() { - final Set> correlatedJoins = new HashSet<>(); + final Set> correlatedJoins = Collections.newSetFromMap( new IdentityHashMap<>() ); final SqmFromClause fromClause = getQuerySpec().getFromClause(); if ( fromClause != null ) { for ( SqmRoot root : fromClause.getRoots() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/StandardLockingClauseStrategy.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/StandardLockingClauseStrategy.java index d3d8799641c8..bd09dc8f34ba 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/StandardLockingClauseStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/StandardLockingClauseStrategy.java @@ -10,21 +10,26 @@ import org.hibernate.dialect.RowLockStrategy; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.mapping.EntityAssociationMapping; +import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.TableDetails; import org.hibernate.metamodel.mapping.ValuedModelPart; import org.hibernate.metamodel.mapping.internal.BasicValuedCollectionPart; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.mutation.EntityTableMapping; import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.spi.LockingClauseStrategy; import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableReferenceJoin; +import org.hibernate.sql.model.TableMapping; import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -212,21 +217,61 @@ private void addTableAliases(TableGroup tableGroup, List lockItems) { } private void addColumnRefs(TableGroup tableGroup, List lockItems) { - Collections.addAll( lockItems, determineKeyColumnRefs( tableGroup ) ); - } - - private String[] determineKeyColumnRefs(TableGroup tableGroup) { final String[] keyColumns = determineKeyColumnNames( tableGroup.getModelPart() ); - final String[] result = new String[keyColumns.length]; final String tableAlias = tableGroup.getPrimaryTableReference().getIdentificationVariable(); for ( int i = 0; i < keyColumns.length; i++ ) { // NOTE: in some tests with Oracle, the qualifiers are being applied twice; // still need to track that down. possibly, unexpected calls to // `Dialect#applyLocksToSql`? assert !keyColumns[i].contains( "." ); - result[i] = tableAlias + "." + keyColumns[i]; + lockItems.add( tableAlias + "." + keyColumns[i] ); + } + + final List tableReferenceJoins = tableGroup.getTableReferenceJoins(); + if ( CollectionHelper.isNotEmpty( tableReferenceJoins ) ) { + final EntityPersister entityPersister = determineEntityPersister( tableGroup.getModelPart() ); + for ( int i = 0; i < tableReferenceJoins.size(); i++ ) { + final TableReferenceJoin tableReferenceJoin = tableReferenceJoins.get( i ); + final NamedTableReference joinedTableReference = tableReferenceJoin.getJoinedTableReference(); + final String tableJoinAlias = joinedTableReference.getIdentificationVariable(); + final TableMapping tableMapping = determineTableMapping( entityPersister, tableReferenceJoin ); + for ( TableDetails.KeyColumn keyColumn : tableMapping.getKeyDetails().getKeyColumns() ) { + lockItems.add( tableJoinAlias + "." + keyColumn.getColumnName() ); + } + } + } + } + + private TableMapping determineTableMapping(EntityPersister entityPersister, TableReferenceJoin tableReferenceJoin) { + final NamedTableReference joinedTableReference = tableReferenceJoin.getJoinedTableReference(); + for ( EntityTableMapping tableMapping : entityPersister.getTableMappings() ) { + if ( joinedTableReference.containsAffectedTableName( tableMapping.getTableName() ) ) { + return tableMapping; + } + } + for ( EntityMappingType subMappingType : entityPersister.getSubMappingTypes() ) { + for ( EntityTableMapping tableMapping : subMappingType.getEntityPersister().getTableMappings() ) { + if ( joinedTableReference.containsAffectedTableName( tableMapping.getTableName() ) ) { + return tableMapping; + } + } + } + throw new IllegalArgumentException( "Couldn't find subclass index for joined table reference " + joinedTableReference ); + } + + private EntityPersister determineEntityPersister(ModelPartContainer modelPart) { + if ( modelPart instanceof EntityPersister entityPersister ) { + return entityPersister; + } + else if ( modelPart instanceof PluralAttributeMapping pluralAttributeMapping ) { + return pluralAttributeMapping.getCollectionDescriptor().getElementPersister(); + } + else if ( modelPart instanceof EntityAssociationMapping entityAssociationMapping ) { + return entityAssociationMapping.getAssociatedEntityMappingType().getEntityPersister(); + } + else { + throw new IllegalArgumentException( "Expected table group with table joins to have an entity typed model part but got: " + modelPart ); } - return result; } private String[] determineKeyColumnNames(ModelPart modelPart) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index 46b3a75d00fd..b2a4cf3dadcb 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -7664,7 +7664,24 @@ public void visitInListPredicate(InListPredicate inListPredicate) { } else if ( !dialect.supportsRowValueConstructorSyntaxInInList() ) { // Some DBs like Oracle support tuples only for the IN subquery predicate - if ( dialect.supportsRowValueConstructorSyntaxInInSubQuery() && dialect.supportsUnionAll() ) { + if ( dialect.supportsRowValueConstructorSyntaxInInSubQuery() && dialect.supportsValuesList() ) { + inListPredicate.getTestExpression().accept( this ); + if ( inListPredicate.isNegated() ) { + appendSql( " not" ); + } + appendSql( " in (select * from (values" ); + char separator = ' '; + for ( Expression expression : listExpressions ) { + appendSql( separator ); + appendSql( OPEN_PARENTHESIS ); + renderCommaSeparated( getSqlTuple( expression ).getExpressions() ); + appendSql( CLOSE_PARENTHESIS ); + separator = ','; + } + appendSql( CLOSE_PARENTHESIS ); + appendSql( CLOSE_PARENTHESIS ); + } + else if ( dialect.supportsRowValueConstructorSyntaxInInSubQuery() && dialect.supportsUnionAll() ) { inListPredicate.getTestExpression().accept( this ); if ( inListPredicate.isNegated() ) { appendSql( " not" ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/MutatingTableReferenceGroupWrapper.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/MutatingTableReferenceGroupWrapper.java index 16f249201022..4047e37ebfb0 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/MutatingTableReferenceGroupWrapper.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/MutatingTableReferenceGroupWrapper.java @@ -62,14 +62,12 @@ public TableReference getTableReference( NavigablePath navigablePath, String tableExpression, boolean resolve) { - return mutatingTableReference.getTableExpression().equals( tableExpression ) - ? mutatingTableReference - : null; + return mutatingTableReference.getTableReference( tableExpression ); } @Override public void applyAffectedTableNames(Consumer nameCollector) { - nameCollector.accept( mutatingTableReference.getTableExpression() ); + mutatingTableReference.applyAffectedTableNames( nameCollector); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java index 84b19926bb2b..35f17c1a6b89 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java @@ -1118,6 +1118,10 @@ else if ( isResultInitializer() ) { else if ( data.entityHolder.getEntityInitializer() != this ) { data.setState( State.INITIALIZED ); } + else if ( data.shallowCached ) { + // For shallow cached entities, only the id is available, so ensure we load the data immediately + data.setInstance( data.entityInstanceForNotify = resolveEntityInstance( data ) ); + } } else if ( ( entityFromExecutionContext = getEntityFromExecutionContext( data ) ) != null ) { // This is the entity to refresh, so don't set the state to initialized @@ -1231,7 +1235,7 @@ protected Object resolveEntityInstance(EntityInitializerData data) { return resolved; } else { - if ( rowProcessingState.isQueryCacheHit() && entityDescriptor.useShallowQueryCacheLayout() ) { + if ( data.shallowCached ) { // We must load the entity this way, because the query cache entry contains only the primary key data.setState( State.INITIALIZED ); final SharedSessionContractImplementor session = rowProcessingState.getSession(); diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/ColumnDefinitions.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/ColumnDefinitions.java index 3130e3ed894f..c68bb8b359d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/ColumnDefinitions.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/ColumnDefinitions.java @@ -144,34 +144,41 @@ private static void appendConstraints( } if ( dialect.supportsColumnCheck() ) { - // some databases (Maria, SQL Server) don't like multiple 'check' clauses final List checkConstraints = column.getCheckConstraints(); - long anonConstraints = checkConstraints.stream().filter(CheckConstraint::isAnonymous).count(); - if ( anonConstraints == 1 ) { - for ( CheckConstraint constraint : checkConstraints ) { - definition.append( constraint.constraintString( dialect ) ); + boolean hasAnonymousConstraints = false; + for ( CheckConstraint constraint : checkConstraints ) { + if ( constraint.isAnonymous() ) { + if ( !hasAnonymousConstraints ) { + definition.append(" check ("); + hasAnonymousConstraints = true; + } + else { + definition.append(" and "); + } + definition.append( constraint.getConstraintInParens() ); } } - else { - boolean first = true; + if ( hasAnonymousConstraints ) { + definition.append( ')' ); + } + + if ( !dialect.supportsTableCheck() ) { + // When table check constraints are not supported, try to render all named constraints for ( CheckConstraint constraint : checkConstraints ) { - if ( constraint.isAnonymous() ) { - if ( first ) { - definition.append(" check ("); - first = false; - } - else { - definition.append(" and "); - } - definition.append( constraint.getConstraintInParens() ); + if ( constraint.isNamed() ) { + definition.append( constraint.constraintString( dialect ) ); } } - if ( !first ) { - definition.append(")"); - } + } + else if ( !hasAnonymousConstraints && dialect.supportsNamedColumnCheck() ) { + // Otherwise only render the first named constraint as column constraint if there are no anonymous + // constraints and named column check constraint are supported, because some database don't like + // multiple check clauses. + // Note that the TableExporter will take care of named constraints then for ( CheckConstraint constraint : checkConstraints ) { if ( constraint.isNamed() ) { definition.append( constraint.constraintString( dialect ) ); + break; } } } diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/StandardTableExporter.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/StandardTableExporter.java index 98358f28aab0..f597d840371e 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/StandardTableExporter.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/StandardTableExporter.java @@ -197,37 +197,43 @@ protected void applyTableTypeString(StringBuilder buf) { protected void applyTableCheck(Table table, StringBuilder buf) { if ( dialect.supportsTableCheck() ) { - if ( !dialect.supportsColumnCheck() ) { - for ( Column column : table.getColumns() ) { - // some databases (Maria, SQL Server) don't like multiple 'check' clauses - final List checkConstraints = column.getCheckConstraints(); - long anonConstraints = checkConstraints.stream().filter( CheckConstraint::isAnonymous ).count(); - if ( anonConstraints == 1 ) { - for ( CheckConstraint constraint : checkConstraints ) { - buf.append( "," ).append( constraint.constraintString( dialect ) ); - } - } - else { - boolean first = true; - for ( CheckConstraint constraint : checkConstraints ) { - if ( constraint.isAnonymous() ) { - if ( first ) { - buf.append( "," ).append( " check (" ); - first = false; - } - else { - buf.append( " and " ); - } - buf.append( constraint.getConstraintInParens() ); + for ( Column column : table.getColumns() ) { + final List checkConstraints = column.getCheckConstraints(); + boolean hasAnonymousConstraints = false; + if ( !dialect.supportsColumnCheck() ) { + for ( CheckConstraint constraint : checkConstraints ) { + if ( constraint.isAnonymous() ) { + if ( !hasAnonymousConstraints ) { + buf.append( ", check (" ); + hasAnonymousConstraints = true; + } + else { + buf.append( " and " ); } + buf.append( constraint.getConstraintInParens() ); } - if ( !first ) { - buf.append( ")" ); + } + if ( hasAnonymousConstraints ) { + buf.append( ')' ); + } + } + else { + hasAnonymousConstraints = checkConstraints.stream().anyMatch( CheckConstraint::isAnonymous ); + } + + // Since some databases don't like when multiple check clauses appear for a colum definition, + // named constraints need to be hoisted to the table definition. + // Skip the first named constraint if the column has no anonymous constraints and the dialect + // supports named column check constraints, because ColumnDefinitions will render the first check + // constraint already. + boolean skipNextNamedConstraint = !hasAnonymousConstraints && dialect.supportsNamedColumnCheck(); + for ( CheckConstraint constraint : checkConstraints ) { + if ( constraint.isNamed() ) { + if ( skipNextNamedConstraint ) { + skipNextNamedConstraint = false; } - for ( CheckConstraint constraint : checkConstraints ) { - if ( constraint.isNamed() ) { - buf.append( constraint.constraintString( dialect ) ); - } + else { + buf.append( ',' ).append( constraint.constraintString( dialect ) ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java index 29aafc2ef259..1ae4f6a69ec1 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java @@ -205,13 +205,12 @@ public EntityMetamodel( idAttributeNames = singleton( identifierAttribute.getName() ); } - bytecodeEnhancementMetadata = BytecodeEnhancementMetadataPojoImpl.from( + bytecodeEnhancementMetadata = getBytecodeEnhancementMetadataPojo( persistentClass, + creationContext, idAttributeNames, nonAggregatedCidMapper, - collectionsInDefaultFetchGroupEnabled, - creationContext.getMetadata() - ); + collectionsInDefaultFetchGroupEnabled ); } else { bytecodeEnhancementMetadata = new BytecodeEnhancementMetadataNonPojoImpl( persistentClass.getEntityName() ); @@ -355,8 +354,9 @@ public EntityMetamodel( propertyInsertability[i] = writePropertyValue( (OnExecutionGenerator) generator ); } foundPostInsertGeneratedValues = foundPostInsertGeneratedValues - || generator instanceof OnExecutionGenerator; + || generatedOnExecution; foundPreInsertGeneratedValues = foundPreInsertGeneratedValues + || !generatedOnExecution || generator instanceof BeforeExecutionGenerator; } else if ( !allowMutation ) { @@ -366,9 +366,10 @@ else if ( !allowMutation ) { if ( generatedOnExecution ) { propertyUpdateability[i] = writePropertyValue( (OnExecutionGenerator) generator ); } - foundPostUpdateGeneratedValues = foundPostUpdateGeneratedValues - || generator instanceof OnExecutionGenerator; - foundPreUpdateGeneratedValues = foundPreUpdateGeneratedValues + foundPostUpdateGeneratedValues = foundPostInsertGeneratedValues + || generatedOnExecution; + foundPreUpdateGeneratedValues = foundPreInsertGeneratedValues + || !generatedOnExecution || generator instanceof BeforeExecutionGenerator; } else if ( !allowMutation ) { @@ -512,6 +513,19 @@ && isAbstractClass( persistentClass.getMappedClass() ) ) { // entityNameByInheritanceClassMap = toSmallMap( entityNameByInheritanceClassMapLocal ); } + /* + * Used by Hibernate Reactive + */ + protected BytecodeEnhancementMetadata getBytecodeEnhancementMetadataPojo(PersistentClass persistentClass, RuntimeModelCreationContext creationContext, Set idAttributeNames, CompositeType nonAggregatedCidMapper, boolean collectionsInDefaultFetchGroupEnabled) { + return BytecodeEnhancementMetadataPojoImpl.from( + persistentClass, + idAttributeNames, + nonAggregatedCidMapper, + collectionsInDefaultFetchGroupEnabled, + creationContext.getMetadata() + ); + } + private static boolean writePropertyValue(OnExecutionGenerator generator) { final boolean writePropertyValue = generator.writePropertyValue(); // TODO: move this validation somewhere else! diff --git a/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java b/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java index 3ce06f230a0f..f96b33957174 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java +++ b/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java @@ -16,11 +16,13 @@ import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.StringHelper; import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; import org.hibernate.type.descriptor.java.BasicPluralJavaType; import org.hibernate.type.descriptor.java.ImmutableMutabilityPlan; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; +import org.hibernate.type.descriptor.jdbc.DelegatingJdbcTypeIndicators; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.internal.BasicTypeImpl; import org.hibernate.type.internal.ConvertedBasicTypeImpl; @@ -162,12 +164,53 @@ private BasicType resolvedType(JavaType javaType, JdbcType jdbcType) { private BasicType resolvedType(ArrayJdbcType arrayType, BasicPluralJavaType castPluralJavaType) { final BasicType elementType = resolve( castPluralJavaType.getElementJavaType(), arrayType.getElementJdbcType() ); + final var indicators = typeConfiguration.getCurrentBaseSqlTypeIndicators(); final BasicType resolvedType = castPluralJavaType.resolveType( typeConfiguration, - typeConfiguration.getCurrentBaseSqlTypeIndicators().getDialect(), + indicators.getDialect(), elementType, - null, - typeConfiguration.getCurrentBaseSqlTypeIndicators() + new ColumnTypeInformation() { + @Override + public Boolean getNullable() { + return null; + } + + @Override + public int getTypeCode() { + return arrayType.getDefaultSqlTypeCode(); + } + + @Override + public String getTypeName() { + return null; + } + + @Override + public int getColumnSize() { + return 0; + } + + @Override + public int getDecimalDigits() { + return 0; + } + }, + new DelegatingJdbcTypeIndicators( indicators ) { + @Override + public Integer getExplicitJdbcTypeCode() { + return arrayType.getDefaultSqlTypeCode(); + } + + @Override + public int getPreferredSqlTypeCodeForArray() { + return arrayType.getDefaultSqlTypeCode(); + } + + @Override + public int getPreferredSqlTypeCodeForArray(int elementSqlTypeCode) { + return arrayType.getDefaultSqlTypeCode(); + } + } ); if ( resolvedType instanceof BasicPluralType ) { register( resolvedType ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java index 6f1d3fc74f63..fcffb0657ff7 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java @@ -169,7 +169,10 @@ protected Object[] getArray(BasicBinder binder, ValueBinder elementBin protected X getArray(BasicExtractor extractor, java.sql.Array array, WrapperOptions options) throws SQLException { final JavaType javaType = extractor.getJavaType(); - if ( array != null && getElementJdbcType() instanceof AggregateJdbcType aggregateJdbcType ) { + if (array != null + && getElementJdbcType() instanceof AggregateJdbcType aggregateJdbcType + && aggregateJdbcType.getEmbeddableMappingType() != null) { + final EmbeddableMappingType embeddableMappingType = aggregateJdbcType.getEmbeddableMappingType(); final Object rawArray = array.getArray(); final Object[] domainObjects = new Object[Array.getLength( rawArray )]; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryTimestampGenerationBatchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryTimestampGenerationBatchTest.java new file mode 100644 index 000000000000..b79d0be58652 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryTimestampGenerationBatchTest.java @@ -0,0 +1,143 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SourceType; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.generator.internal.CurrentTimestampGeneration; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.hibernate.testing.orm.junit.SettingProvider; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.stream.IntStream; + +import static java.lang.Thread.sleep; +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; + +@DomainModel(annotatedClasses = InMemoryTimestampGenerationBatchTest.Person.class) +@SessionFactory(generateStatistics = true) +@ServiceRegistry(settings = @Setting(name = AvailableSettings.STATEMENT_BATCH_SIZE, value = "5"), + settingProviders = @SettingProvider(settingName = CurrentTimestampGeneration.CLOCK_SETTING_NAME, + provider = InMemoryTimestampGenerationBatchTest.MutableClockProvider.class)) +@Jira("https://hibernate.atlassian.net/browse/HHH-19840") +public class InMemoryTimestampGenerationBatchTest { + private static final MutableClock clock = new MutableClock(); + + private static final int PERSON_COUNT = 8; + + @Test + public void test(SessionFactoryScope scope) throws InterruptedException { + final var statistics = scope.getSessionFactory().getStatistics(); + scope.inTransaction( session -> { + Person person = null; + for ( int i = 1; i <= PERSON_COUNT; i++ ) { + person = new Person(); + person.setId( (long) i ); + person.setName( "person_" + i ); + session.persist( person ); + } + + statistics.clear(); + session.flush(); + + assertEquals( 1, statistics.getPrepareStatementCount(), "Expected updates to execute in batches" ); + + assertNotNull( person.getCreatedOn() ); + assertNotNull( person.getUpdatedOn() ); + } ); + + + clock.tick(); + sleep( 1 ); + + scope.inTransaction( session -> { + final var persons = session.findMultiple( Person.class, + IntStream.rangeClosed( 1, PERSON_COUNT ) + .mapToObj( i -> (long) i ) + .toList() ); + + assertThat( persons ).hasSize( PERSON_COUNT ); + assertThat( persons ).doesNotContainNull(); + + Person person = null; + for ( final Person p : persons ) { + p.setName( p.getName() + "_updated" ); + person = p; + } + + final var createdOn = person.getCreatedOn(); + final var updatedOn = person.getUpdatedOn(); + + statistics.clear(); + session.flush(); + + assertEquals( 1, statistics.getPrepareStatementCount(), "Expected updates to execute in batches" ); + + assertEquals( person.getCreatedOn(), createdOn ); + assertTrue( person.getUpdatedOn().isAfter( updatedOn ) ); + } ); + } + + public static class MutableClockProvider implements SettingProvider.Provider { + @Override + public Object getSetting() { + return clock; + } + } + + @Entity(name = "Person") + public static class Person { + @Id + private Long id; + + private String name; + + @Column(nullable = false) + @CreationTimestamp(source = SourceType.VM) + private Instant createdOn; + + @Column(nullable = false) + @UpdateTimestamp(source = SourceType.VM) + private Instant updatedOn; + + 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 Instant getCreatedOn() { + return createdOn; + } + + public Instant getUpdatedOn() { + return updatedOn; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryTimestampGenerationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryTimestampGenerationTest.java new file mode 100644 index 000000000000..9854a794646b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryTimestampGenerationTest.java @@ -0,0 +1,122 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SourceType; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.generator.internal.CurrentTimestampGeneration; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.SettingProvider; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static java.lang.Thread.sleep; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Jpa(annotatedClasses = InMemoryTimestampGenerationTest.Person.class, + settingProviders = @SettingProvider(settingName = CurrentTimestampGeneration.CLOCK_SETTING_NAME, + provider = InMemoryTimestampGenerationTest.MutableClockProvider.class)) +@Jira("https://hibernate.atlassian.net/browse/HHH-19840") +public class InMemoryTimestampGenerationTest { + private static final MutableClock clock = new MutableClock(); + + @Test + public void test(EntityManagerFactoryScope scope) throws InterruptedException { + scope.inTransaction( entityManager -> { + Person person = new Person(); + person.setId( 1L ); + person.setFirstName( "Jon" ); + person.setLastName( "Doe" ); + entityManager.persist( person ); + + entityManager.flush(); + + assertNotNull( person.getCreatedOn() ); + assertNotNull( person.getUpdatedOn() ); + } ); + + clock.tick(); + sleep( 1 ); + + scope.inTransaction( entityManager -> { + final Person person = entityManager.find( Person.class, 1L ); + person.setLastName( "Doe Jr." ); + + final var updatedOn = person.getUpdatedOn(); + final var createdOn = person.getCreatedOn(); + + entityManager.flush(); + + assertEquals( person.getCreatedOn(), createdOn ); + assertTrue( person.getUpdatedOn().isAfter( updatedOn ) ); + } ); + } + + static class MutableClockProvider implements SettingProvider.Provider { + @Override + public Object getSetting() { + return clock; + } + } + + @Entity(name = "Person") + static class Person { + @Id + private Long id; + + private String firstName; + + private String lastName; + + @Column(nullable = false) + @CreationTimestamp(source= SourceType.VM) + private Instant createdOn; + + @Column(nullable = false) + @UpdateTimestamp(source= SourceType.VM) + private Instant updatedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = 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 Instant getCreatedOn() { + return createdOn; + } + + public Instant getUpdatedOn() { + return updatedOn; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryUpdateTimestampTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryUpdateTimestampTest.java deleted file mode 100644 index 62c81dd0cc9b..000000000000 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryUpdateTimestampTest.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.orm.test.annotations; - -import java.util.Date; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; - -import org.hibernate.annotations.UpdateTimestamp; -import org.hibernate.generator.internal.CurrentTimestampGeneration; -import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; - -import org.hibernate.testing.orm.junit.JiraKey; -import org.junit.Assert; -import org.junit.Test; - -import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; -import static org.junit.Assert.assertTrue; - -/** - * @author Vlad Mihalcea - */ -@JiraKey("HHH-13256") -public class InMemoryUpdateTimestampTest extends BaseEntityManagerFunctionalTestCase { - - private static final MutableClock clock = new MutableClock(); - - @Override - protected Class[] getAnnotatedClasses() { - return new Class[] { - Person.class - }; - } - - @Override - protected void addConfigOptions(Map options) { - super.addConfigOptions( options ); - options.put( CurrentTimestampGeneration.CLOCK_SETTING_NAME, clock ); - } - - @Test - public void test() { - doInJPA( this::entityManagerFactory, entityManager -> { - Person person = new Person(); - person.setId( 1L ); - person.setFirstName( "Jon" ); - person.setLastName( "Doe" ); - entityManager.persist( person ); - - entityManager.flush(); - Assert.assertNotNull( person.getUpdatedOn() ); - } ); - clock.tick(); - - AtomicReference beforeTimestamp = new AtomicReference<>(); - - sleep( 1 ); - - Person _person = doInJPA( this::entityManagerFactory, entityManager -> { - Person person = entityManager.find( Person.class, 1L ); - beforeTimestamp.set( person.getUpdatedOn() ); - person.setLastName( "Doe Jr." ); - - return person; - } ); - - assertTrue( _person.getUpdatedOn().after( beforeTimestamp.get() ) ); - } - - @Entity(name = "Person") - public static class Person { - - @Id - private Long id; - - private String firstName; - - private String lastName; - - @Column(nullable = false) - @UpdateTimestamp - private Date updatedOn; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = 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 Date getUpdatedOn() { - return updatedOn; - } - } -} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/EmbeddedIdLazyOneToOneCriteriaQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/EmbeddedIdLazyOneToOneCriteriaQueryTest.java new file mode 100644 index 000000000000..34ab1c467a9d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/EmbeddedIdLazyOneToOneCriteriaQueryTest.java @@ -0,0 +1,117 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.cid; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.hibernate.Hibernate; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityA.class, + EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityB.class, +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19687") +@BytecodeEnhanced +public class EmbeddedIdLazyOneToOneCriteriaQueryTest { + + @Test + public void query(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final CriteriaBuilder builder = session.getCriteriaBuilder(); + final CriteriaQuery criteriaQuery = builder.createQuery( EntityA.class ); + final Root root = criteriaQuery.from( EntityA.class ); + criteriaQuery.where( root.get( "id" ).in( 1 ) ); + criteriaQuery.select( root ); + + final List entities = session.createQuery( criteriaQuery ).getResultList(); + assertThat( entities ).hasSize( 1 ); + assertThat( Hibernate.isPropertyInitialized( entities.get( 0 ), "entityB" ) ).isFalse(); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityA entityA = new EntityA( 1 ); + session.persist( entityA ); + final EntityB entityB = new EntityB( new EntityBId( entityA ) ); + session.persist( entityB ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> session.getSessionFactory().getSchemaManager().truncateMappedObjects() ); + } + + @Entity(name = "EntityA") + static class EntityA { + + @Id + private Integer id; + + @OneToOne(mappedBy = "id.entityA", fetch = FetchType.LAZY) + private EntityB entityB; + + public EntityA() { + } + + public EntityA(Integer id) { + this.id = id; + } + + } + + @Entity(name = "EntityB") + static class EntityB { + + @EmbeddedId + private EntityBId id; + + public EntityB() { + } + + public EntityB(EntityBId id) { + this.id = id; + } + + } + + @Embeddable + static class EntityBId { + + @OneToOne(fetch = FetchType.LAZY) + private EntityA entityA; + + public EntityBId() { + } + + public EntityBId(EntityA entityA) { + this.entityA = entityA; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/ForeignPackageSuperclassAccessorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/ForeignPackageSuperclassAccessorTest.java index a2fae4213692..0d81e937a972 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/ForeignPackageSuperclassAccessorTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/ForeignPackageSuperclassAccessorTest.java @@ -6,6 +6,7 @@ import org.hibernate.orm.test.bytecode.foreignpackage.ConcreteEntity; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.SessionFactory; @@ -18,6 +19,7 @@ SuperclassEntity.class }) @Jira("https://hibernate.atlassian.net/browse/HHH-19369") +@BytecodeEnhanced(runNotEnhancedAsWell = true) public class ForeignPackageSuperclassAccessorTest { @Test diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/cid/EmbeddedIdInParameterBindingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/cid/EmbeddedIdInParameterBindingTest.java new file mode 100644 index 000000000000..83f29afcfe38 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/cid/EmbeddedIdInParameterBindingTest.java @@ -0,0 +1,249 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.cid; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + EmbeddedIdInParameterBindingTest.Delivery.class + } +) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = AvailableSettings.DIALECT_NATIVE_PARAM_MARKERS, value = "true")) +@Jira( "HHH-19792" ) +public class EmbeddedIdInParameterBindingTest { + + LocationId verbania = new LocationId( "Italy", "Verbania" ); + Delivery pizza = new Delivery( verbania, "Pizza Margherita" ); + + LocationId hallein = new LocationId( "Austria", "Hallein" ); + Delivery schnitzel = new Delivery( hallein, "Wiener Schnitzel" ); + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist( pizza ); + session.persist( schnitzel ); + } + ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncate(); + } + + @Test + public void testQueryWithWhereClauseContainingInOperator(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List deliveries = session.createQuery( "from Delivery d where d.locationId in (?1)", + Delivery.class ) + .setParameter( 1, verbania ) + .getResultList(); + assertThat( deliveries.size() ).isEqualTo( 1 ); + assertThat( deliveries ).contains( pizza ); + } + ); + } + + @Test + public void testQueryWithWhereClauseContainingInOperatorWithListOfParametersValues(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List deliveries = session.createQuery( "from Delivery d where d.locationId in ?1", + Delivery.class ) + .setParameter( 1, List.of( verbania ) ) + .getResultList(); + assertThat( deliveries.size() ).isEqualTo( 1 ); + assertThat( deliveries ).contains( pizza ); + } + ); + + scope.inTransaction( + session -> { + List deliveries = session.createQuery( "from Delivery d where d.locationId in ?1", + Delivery.class ) + .setParameter( 1, List.of( verbania, schnitzel ) ) + .getResultList(); + assertThat( deliveries.size() ).isEqualTo( 2 ); + assertThat( deliveries ).contains( pizza ); + assertThat( deliveries ).contains( schnitzel ); + } + ); + } + + @Test + public void testMoreComplexWhereClause(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List deliveries = session.createQuery( + "from Delivery d where d.field2Copy = ?3 and d.locationId in (?1,?2) and d.field = ?3", + Delivery.class ) + .setParameter( 1, verbania ) + .setParameter( 2, hallein ) + .setParameter( 3, "Pizza Margherita" ) + .getResultList(); + assertThat( deliveries.size() ).isEqualTo( 1 ); + assertThat( deliveries ).contains( pizza ); + } + ); + } + + @Test + public void testQueryWithWhereClauseContainingInOperatorAndTwoParamaters(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List deliveries = session.createQuery( "from Delivery d where d.locationId in (?1,?2)", + Delivery.class ) + .setParameter( 1, verbania ) + .setParameter( 2, hallein ) + .getResultList(); + assertThat( deliveries.size() ).isEqualTo( 2 ); + assertThat( deliveries ).contains( pizza ); + assertThat( deliveries ).contains( schnitzel ); + } + ); + } + + @Entity(name = "Delivery") + @Table(name = "Delivery") + public static class Delivery { + + @EmbeddedId + private LocationId locationId; + + @Column(name = "field") + private String field; + + @Column(name = "field2") + private String field2Copy; + + public Delivery() { + } + + public Delivery(LocationId locationId, String field) { + this.locationId = locationId; + this.field = field; + this.field2Copy = field; + } + + public LocationId getLocationId() { + return locationId; + } + + public void setLocationId(LocationId locationId) { + this.locationId = locationId; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + @Override + public String toString() { + return locationId + ":" + field; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Delivery table = (Delivery) o; + return Objects.equals( locationId, table.locationId ) && Objects.equals( field, table.field ); + } + + @Override + public int hashCode() { + return Objects.hash( locationId, field ); + } + } + + + @Embeddable + public static class LocationId { + + @Column(name = "sp_country") + private String country; + + @Column(name = "sp_city") + private String city; + + public LocationId(String country, String city) { + this.country = country; + this.city = city; + } + + public LocationId() { + } + + public String getCountry() { + return country; + } + + public String getCity() { + return city; + } + + public void setCountry(String country) { + this.country = country; + } + + public void setCity(String city) { + this.city = city; + } + + @Override + public String toString() { + return "[" + country + "-" + city + "]"; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + LocationId tableId = (LocationId) o; + return Objects.equals( country, tableId.country ) && Objects.equals( city, tableId.city ); + } + + @Override + public int hashCode() { + return Objects.hash( country, city ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/locktimeout/HANALockTimeoutTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/locktimeout/HANALockTimeoutTest.java index 85b21d5f0123..22e24823d8c2 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/locktimeout/HANALockTimeoutTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/locktimeout/HANALockTimeoutTest.java @@ -55,11 +55,11 @@ public void testLockTimeoutNoAliasNoWait() { @Test public void testLockTimeoutNoAliasSkipLocked() { assertEquals( - " for update", + " for update ignore locked", dialect.getForUpdateString( new LockOptions( LockMode.PESSIMISTIC_READ ).setTimeout( SKIP_LOCKED ) ) ); assertEquals( - " for update", + " for update ignore locked", dialect.getForUpdateString( new LockOptions( LockMode.PESSIMISTIC_WRITE ).setTimeout( SKIP_LOCKED ) ) ); } @@ -100,7 +100,7 @@ public void testLockTimeoutAliasNoWait() { public void testLockTimeoutAliasSkipLocked() { String alias = "a"; assertEquals( - " for update of a", + " for update of a ignore locked", dialect.getForUpdateString( alias, new LockOptions( LockMode.PESSIMISTIC_READ ) @@ -108,7 +108,7 @@ public void testLockTimeoutAliasSkipLocked() { ) ); assertEquals( - " for update of a", + " for update of a ignore locked", dialect.getForUpdateString( alias, new LockOptions( LockMode.PESSIMISTIC_WRITE ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/sequence/DB2zSequenceInformationExtractorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/sequence/DB2zSequenceInformationExtractorTest.java index c9d196883d59..c411bbc21eb6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/sequence/DB2zSequenceInformationExtractorTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/sequence/DB2zSequenceInformationExtractorTest.java @@ -26,7 +26,7 @@ public Dialect getDialect() { @Override public String expectedQuerySequencesString() { - return "select * from sysibm.syssequences"; + return "select case when seqtype='A' then seqschema else schema end as seqschema, case when seqtype='A' then seqname else name end as seqname, start, minvalue, maxvalue, increment from sysibm.syssequences"; } @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dirtiness/SessionIsDirtyTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dirtiness/SessionIsDirtyTests.java new file mode 100644 index 000000000000..25257528c9df --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dirtiness/SessionIsDirtyTests.java @@ -0,0 +1,215 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.dirtiness; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import org.hibernate.Hibernate; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.cache.spi.CacheImplementor; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@DomainModel(annotatedClasses = { + SessionIsDirtyTests.EntityA.class, + SessionIsDirtyTests.EntityB.class, + SessionIsDirtyTests.EntityC.class, +}) +@ServiceRegistry(settings = { + @Setting(name = AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, value = "5"), + @Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true"), +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19605") +public class SessionIsDirtyTests { + @Test + public void testBatchAndCacheDirtiness(SessionFactoryScope scope) { + final CacheImplementor cache = scope.getSessionFactory().getCache(); + cache.evictAllRegions(); + scope.inTransaction( session -> { + final List resultList = session.createSelectionQuery( + "select a from EntityA a order by a.id", + EntityA.class + ).getResultList(); + assertThat( session.isDirty() ).isFalse(); + + assertThat( resultList ).hasSize( 2 ); + final EntityA entityA1 = resultList.get( 0 ); + assertThat( entityA1.getId() ).isEqualTo( 1L ); + assertThat( entityA1.getName() ).isEqualTo( "A1" ); + assertThat( entityA1.getEntityB() ).isNull(); + + final EntityA entityA2 = resultList.get( 1 ); + assertThat( entityA2.getId() ).isEqualTo( 2L ); + assertThat( entityA2.getName() ).isEqualTo( "A2" ); + assertThat( entityA2.getEntityB() ).isNotNull(); + assertThat( entityA2.getEntityB().getEntityA() ).isSameAs( entityA1 ); + + entityA2.getEntityB().setName( "B1 updated" ); + assertThat( session.isDirty() ).isTrue(); + } ); + } + + @Test + public void testLazyAssociationDirtiness(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List resultList = session.createSelectionQuery( + "select c from EntityC c order by c.id", + EntityC.class + ).getResultList(); + assertThat( session.isDirty() ).isFalse(); + + assertThat( resultList ).hasSize( 1 ); + final EntityC entityC = resultList.get( 0 ); + assertThat( entityC.getId() ).isEqualTo( 1L ); + assertThat( entityC.getName() ).isEqualTo( "C1" ); + assertThat( Hibernate.isInitialized( entityC.getEntityB() ) ).isFalse(); + + entityC.getEntityB().setName( "B1 lazy updated" ); + assertThat( session.isDirty() ).isTrue(); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityA entityA1 = new EntityA( 1L, "A1" ); + final EntityA entityA2 = new EntityA( 2L, "A2" ); + final EntityB entityB = new EntityB( 1L, "B1" ); + entityB.entityA = entityA1; + entityA2.entityB = entityB; + session.persist( entityA1 ); + session.persist( entityA2 ); + session.persist( entityB ); + + final EntityC entityC = new EntityC( 1L, "C1" ); + entityC.entityB = entityB; + session.persist( entityC ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "EntityA") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + static class EntityA { + @Id + Long id; + + String name; + + @ManyToOne + @JoinColumn(name = "entity_b") + EntityB entityB; + + public EntityA() { + } + + public EntityA(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public EntityB getEntityB() { + return entityB; + } + } + + @Entity(name = "EntityB") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + static class EntityB { + @Id + Long id; + + String name; + + @ManyToOne + @JoinColumn(name = "entity_a") + EntityA entityA; + + public EntityB() { + } + + public EntityB(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public EntityA getEntityA() { + return entityA; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity(name = "EntityC") + static class EntityC { + @Id + Long id; + + String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "entity_b") + EntityB entityB; + + public EntityC() { + } + + public EntityC(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public EntityB getEntityB() { + return entityB; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitymode/map/basic/DynamicClassTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitymode/map/basic/DynamicClassTest.java index cdb379c619db..3020d8d6314f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/entitymode/map/basic/DynamicClassTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitymode/map/basic/DynamicClassTest.java @@ -4,95 +4,110 @@ */ package org.hibernate.orm.test.entitymode.map.basic; +import org.hibernate.Hibernate; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import org.hibernate.Hibernate; -import org.hibernate.Session; -import org.hibernate.Transaction; -import org.hibernate.cfg.Configuration; - -import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; -import org.junit.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; /** * @author Gavin King */ -public class DynamicClassTest extends BaseCoreFunctionalTestCase { - - @Override - protected String getBaseForMappings() { - return "org/hibernate/orm/test/"; - } - - @Override - public String[] getMappings() { - return new String[] { "entitymode/map/basic/ProductLine.hbm.xml" }; - } - - @Override - public void configure(Configuration cfg) { - } +@SessionFactory +@DomainModel( + xmlMappings = "org/hibernate/orm/test/entitymode/map/basic/ProductLine.hbm.xml" +) +class DynamicClassTest { @Test - public void testLazyDynamicClass() { - Session s = openSession(); - Transaction t = s.beginTransaction(); - - Map cars = new HashMap(); - cars.put("description", "Cars"); - Map monaro = new HashMap(); - monaro.put("productLine", cars); - monaro.put("name", "monaro"); - monaro.put("description", "Holden Monaro"); - Map hsv = new HashMap(); - hsv.put("productLine", cars); - hsv.put("name", "hsv"); - hsv.put("description", "Holden Commodore HSV"); - List models = new ArrayList(); - cars.put("models", models); - models.add(hsv); - models.add(monaro); - s.persist("ProductLine", cars); - t.commit(); - s.close(); - - s = openSession(); - t = s.beginTransaction(); - - cars = (Map) s.createQuery("from ProductLine pl order by pl.description").uniqueResult(); - models = (List) cars.get("models"); - assertFalse( Hibernate.isInitialized(models) ); - assertEquals( models.size(), 2); - assertTrue( Hibernate.isInitialized(models) ); - - s.clear(); - - List list = s.createQuery("from Model m").list(); - for ( Iterator i=list.iterator(); i.hasNext(); ) { - assertFalse( Hibernate.isInitialized( ( (Map) i.next() ).get("productLine") ) ); - } - Map model = (Map) list.get(0); - assertTrue( ( (List) ( (Map) model.get("productLine") ).get("models") ).contains(model) ); - s.clear(); - - t.commit(); - s.close(); - - s = openSession(); - t = s.beginTransaction(); - cars = (Map) s.createQuery("from ProductLine pl order by pl.description").uniqueResult(); - s.remove(cars); - t.commit(); - s.close(); + void testLazyDynamicClass(SessionFactoryScope scope) { + scope.inTransaction( s -> { + Map cars = new HashMap<>(); + cars.put( "description", "Cars" ); + Map monaro = new HashMap<>(); + monaro.put( "productLine", cars ); + monaro.put( "name", "monaro" ); + monaro.put( "description", "Holden Monaro" ); + Map hsv = new HashMap<>(); + hsv.put( "productLine", cars ); + hsv.put( "name", "hsv" ); + hsv.put( "description", "Holden Commodore HSV" ); + List> models = new ArrayList<>(); + cars.put( "models", models ); + models.add( hsv ); + models.add( monaro ); + s.persist( "ProductLine", cars ); + } ); + + scope.inTransaction( s -> { + Map cars = (Map) s.createQuery( + "from ProductLine pl order by pl.description" ).uniqueResult(); + List> models = (List>) cars.get( "models" ); + assertFalse( Hibernate.isInitialized( models ) ); + assertEquals( 2, models.size() ); + assertTrue( Hibernate.isInitialized( models ) ); + + s.clear(); + + List list = s.createQuery( "from Model m" ).list(); + for ( Iterator i = list.iterator(); i.hasNext(); ) { + assertFalse( Hibernate.isInitialized( ((Map) i.next()).get( "productLine" ) ) ); + } + Map model = (Map) list.get( 0 ); + assertTrue( ((List>) ((Map) model.get( "productLine" )).get( + "models" )).contains( model ) ); + s.clear(); + + } ); + + scope.inTransaction( s -> { + Map cars = (Map) s.createQuery( + "from ProductLine pl order by pl.description" ).uniqueResult(); + s.remove( cars ); + } ); } + @Test + void multiload(SessionFactoryScope scope) { + final Object id = scope.fromTransaction( s -> { + Map cars = new HashMap<>(); + cars.put( "description", "Cars" ); + Map monaro = new HashMap<>(); + monaro.put( "productLine", cars ); + monaro.put( "name", "monaro" ); + monaro.put( "description", "Holden Monaro" ); + Map hsv = new HashMap<>(); + hsv.put( "productLine", cars ); + hsv.put( "name", "hsv" ); + hsv.put( "description", "Holden Commodore HSV" ); + List> models = new ArrayList<>(); + cars.put( "models", models ); + models.add( hsv ); + models.add( monaro ); + s.persist( "ProductLine", cars ); + + return cars.get( "id" ); + } ); + + scope.inTransaction( s -> { + var rootGraph = s.getSessionFactory().createGraphForDynamicEntity( "ProductLine" ); + + List> found = s.findMultiple( rootGraph, List.of( id ) ); + + assertThat( found ).hasSize( 1 ); + } ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java index e255416ba04f..f1faea74a561 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java @@ -4,6 +4,7 @@ */ package org.hibernate.orm.test.function.array; +import java.util.Arrays; import java.util.List; import org.hibernate.boot.ResourceStreamLocator; @@ -11,6 +12,7 @@ import org.hibernate.boot.spi.AdditionalMappingContributor; import org.hibernate.boot.spi.InFlightMetadataCollector; import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.type.OracleArrayJdbcType; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.SpannerDialect; @@ -18,6 +20,7 @@ import org.hibernate.query.criteria.JpaCriteriaQuery; import org.hibernate.query.criteria.JpaRoot; import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.testing.orm.junit.RequiresDialect; import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.java.ArrayJavaType; import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; @@ -35,6 +38,7 @@ import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.testing.orm.junit.Jira; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -170,4 +174,21 @@ public void testNonExistingArrayType(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19681") + @RequiresDialect(PostgreSQLDialect.class) + public void testJsonBJdbcArray(SessionFactoryScope scope) { + scope.inTransaction( session -> { + String sql = "select groupId, array_agg(json_values) " + + "from (VALUES (1,'[1,2]'::jsonb),(1,'[10,20]'::jsonb)) as row(groupId,json_values) " + + "group by groupId"; + + List result = session.createNativeQuery(sql, Object[].class).getResultList(); + assertEquals(1,result.size()); + assertEquals(2, result.get(0).length); + assertEquals( 1,result.get(0)[0] ); + assertEquals( "[[1, 2], [10, 20]]", Arrays.toString((String[])result.get(0)[1]) ); + } ); + } + } diff --git a/hibernate-core/src/test/java/x/IdClassSingleOneToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/IdClassSingleOneToOneTest.java similarity index 93% rename from hibernate-core/src/test/java/x/IdClassSingleOneToOneTest.java rename to hibernate-core/src/test/java/org/hibernate/orm/test/idclass/IdClassSingleOneToOneTest.java index be3f172e2d31..3a6d90ae201c 100644 --- a/hibernate-core/src/test/java/x/IdClassSingleOneToOneTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/IdClassSingleOneToOneTest.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package x; +package org.hibernate.orm.test.idclass; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -10,6 +10,7 @@ import jakarta.persistence.IdClass; import jakarta.persistence.OneToOne; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Test; @@ -25,6 +26,7 @@ IdClassSingleOneToOneTest.EntityB.class, } ) @SessionFactory +@Jira(value = "https://hibernate.atlassian.net/browse/HHH-19688") public class IdClassSingleOneToOneTest { @Test diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedDiscSameAttributeNameTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedDiscSameAttributeNameTest.java new file mode 100644 index 000000000000..e63f69101acc --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedDiscSameAttributeNameTest.java @@ -0,0 +1,157 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.inheritance.discriminator; + +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + JoinedDiscSameAttributeNameTest.Ancestor.class, + JoinedDiscSameAttributeNameTest.DescendantA.class, + JoinedDiscSameAttributeNameTest.DescendantB.class, + JoinedDiscSameAttributeNameTest.DescendantTak.class, + JoinedDiscSameAttributeNameTest.DescendantD.class, +}) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-19756" ) +public class JoinedDiscSameAttributeNameTest { + @Test + void testCoalesceSameType(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var query = cb.createTupleQuery(); + final var root = query.from( Ancestor.class ); + final var dscCRoot = cb.treat( root, DescendantTak.class ); + + query.select( cb.tuple( + root.get( JoinedDiscSameAttributeNameTest_.Ancestor_.id ).alias( "id" ), + cb.coalesce( + dscCRoot.get( JoinedDiscSameAttributeNameTest_.DescendantTak_.subtitle ), + dscCRoot.get( JoinedDiscSameAttributeNameTest_.DescendantTak_.title ) + ).alias( "description" ) + ) ).orderBy( cb.asc( root.get( JoinedDiscSameAttributeNameTest_.Ancestor_.id ) ) ); + + final var resultList = session.createSelectionQuery( query ).getResultList(); + assertResults( resultList, null, "title", null ); + } ); + } + + @Test + void testCoalesceDifferentTypes(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var query = cb.createTupleQuery(); + final var root = query.from( Ancestor.class ); + final var dscARoot = cb.treat( root, DescendantA.class ); + final var dscCRoot = cb.treat( root, DescendantTak.class ); + final var dscDRoot = cb.treat( root, DescendantD.class ); + + query.select( cb.tuple( + root.get( JoinedDiscSameAttributeNameTest_.Ancestor_.id ).alias( "id" ), + cb.coalesce( + dscDRoot.get( JoinedDiscSameAttributeNameTest_.DescendantD_.subtitle ), + cb.coalesce( + cb.coalesce( + dscARoot.get( JoinedDiscSameAttributeNameTest_.DescendantA_.subtitle ), + dscARoot.get( JoinedDiscSameAttributeNameTest_.DescendantA_.title ) + ), + cb.coalesce( + dscCRoot.get( JoinedDiscSameAttributeNameTest_.DescendantTak_.subtitle ), + dscCRoot.get( JoinedDiscSameAttributeNameTest_.DescendantTak_.title ) + ) + ) + ).alias( "description" ) + ) ).orderBy( cb.asc( root.get( JoinedDiscSameAttributeNameTest_.Ancestor_.id ) ) ); + + final var resultList = session.createSelectionQuery( query ).getResultList(); + assertResults( resultList, null, "title", "subtitle" ); + } ); + } + + private static void assertResults(List resultList, String... expected) { + assertThat( resultList ).hasSize( expected.length ); + for ( int i = 0; i < expected.length; i++ ) { + final var r = resultList.get( i ); + assertThat( r.get( 0, Integer.class) ).isEqualTo( i + 1 ); + assertThat( r.get( 1, String.class ) ).isEqualTo( expected[i] ); + } + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var descendantA = new DescendantA(); + descendantA.id = 1; + session.persist( descendantA ); + final var descendantTak = new DescendantTak(); + descendantTak.id = 2; + descendantTak.title = "title"; + session.persist( descendantTak ); + final var descendantD = new DescendantD(); + descendantD.id = 3; + descendantD.subtitle = "subtitle"; + session.persist( descendantD ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "Ancestor") + @Table(name = "t_ancestor") + @Inheritance(strategy = InheritanceType.JOINED) + @DiscriminatorColumn(name = "def_type_id") + static abstract class Ancestor { + @Id + Integer id; + } + + @Entity(name = "DescendantA") + @DiscriminatorValue("A") + @Table(name = "t_descendant_a") + static class DescendantA extends Ancestor { + String title; + String subtitle; + } + + @Entity(name = "DescendantB") + @Table(name = "t_descendant_b") + static abstract class DescendantB extends Ancestor { + } + + @Entity(name = "DescendantTak") + @DiscriminatorValue("C") + @Table(name = "t_descendant_c") + static class DescendantTak extends DescendantB { + String title; + String subtitle; + } + + @Entity(name = "DescendantD") + @DiscriminatorValue("D") + @Table(name = "t_descendant_d") + static class DescendantD extends DescendantB { + String subtitle; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/EmbeddableInheritanceRecursiveTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/EmbeddableInheritanceRecursiveTest.java new file mode 100644 index 000000000000..59087b1b4e52 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/EmbeddableInheritanceRecursiveTest.java @@ -0,0 +1,232 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.inheritance.embeddable; + +import org.hibernate.MappingException; +import org.hibernate.boot.Metadata; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; + +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.util.ServiceRegistryUtil; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +@Jira("https://hibernate.atlassian.net/browse/HHH-19648") +public class EmbeddableInheritanceRecursiveTest { + @Test + public void testSimpleRecursiveEmbedded() { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + final MetadataSources metadataSources = new MetadataSources( registryBuilder.build() ) + .addAnnotatedClass( Root1.class ) + .addAnnotatedClass( Entity1.class ); + try { + final Metadata metadata = metadataSources.buildMetadata(); + fail( "Expected MappingException due to recursive embeddable mapping" ); + } + catch (Exception e) { + assertThat( e ).isInstanceOf( MappingException.class ); + assertThat( e.getMessage() ) + .contains( "Recursive embeddable mapping detected" ) + .contains( Root1.class.getName() ); + } + } + + @Test + public void testChildWithRootProp() { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + final MetadataSources metadataSources = new MetadataSources( registryBuilder.build() ) + .addAnnotatedClass( Root2.class ) + .addAnnotatedClass( Child2.class ) + .addAnnotatedClass( Entity2.class ); + try { + final Metadata metadata = metadataSources.buildMetadata(); + fail( "Expected MappingException due to recursive embeddable mapping" ); + } + catch (Exception e) { + assertThat( e ).isInstanceOf( MappingException.class ); + assertThat( e.getMessage() ) + .contains( "Recursive embeddable mapping detected" ) + .contains( Root2.class.getName() ); + } + } + + @Test + public void testRootWithChildProp() { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + final MetadataSources metadataSources = new MetadataSources( registryBuilder.build() ) + .addAnnotatedClass( Root3.class ) + .addAnnotatedClass( Child3.class ) + .addAnnotatedClass( Entity3.class ); + try { + final Metadata metadata = metadataSources.buildMetadata(); + fail( "Expected MappingException due to recursive embeddable mapping" ); + } + catch (Exception e) { + assertThat( e ).isInstanceOf( MappingException.class ); + assertThat( e.getMessage() ) + .contains( "Recursive embeddable mapping detected" ) + .contains( Root3.class.getName() ); + } + } + + @Test + public void testMidEmbedded() { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + final MetadataSources metadataSources = new MetadataSources( registryBuilder.build() ) + .addAnnotatedClass( Root4.class ) + .addAnnotatedClass( Mid4.class ) + .addAnnotatedClass( Child4.class ) + .addAnnotatedClass( Entity4.class ); + try { + final Metadata metadata = metadataSources.buildMetadata(); + fail( "Expected MappingException due to recursive embeddable mapping" ); + } + catch (Exception e) { + assertThat( e ).isInstanceOf( MappingException.class ); + assertThat( e.getMessage() ) + .contains( "Recursive embeddable mapping detected" ) + .contains( Root4.class.getName() ); + } + } + + @Test + public void testUnrelatedRecursive() { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + final MetadataSources metadataSources = new MetadataSources( registryBuilder.build() ) + .addAnnotatedClass( EmbA.class ) + .addAnnotatedClass( EmbB.class ) + .addAnnotatedClass( Entity5.class ); + try { + final Metadata metadata = metadataSources.buildMetadata(); + fail( "Expected MappingException due to recursive embeddable mapping" ); + } + catch (Exception e) { + assertThat( e ).isInstanceOf( MappingException.class ); + assertThat( e.getMessage() ) + .contains( "Recursive embeddable mapping detected" ) + .contains( EmbA.class.getName() ); + } + } + + @Embeddable + static class Root1 { + String root1Prop; + + @Embedded + Root1 nested1; + } + + @Entity(name = "Entity1") + static class Entity1 { + @Id + private Long id; + + @Embedded + private Root1 root1; + } + + @Embeddable + static class Root2 { + String root2Prop; + } + + @Embeddable + static class Child2 extends Root2 { + String child2Prop; + + @Embedded + Root2 nested2; + } + + @Entity(name = "Entity2") + static class Entity2 { + @Id + private Long id; + + @Embedded + private Root2 root2; + } + + @Embeddable + static class Root3 { + String root3Prop; + + @Embedded + Child3 nested3; + } + + @Embeddable + static class Child3 extends Root3 { + String child3Prop; + } + + @Entity(name = "Entity3") + static class Entity3 { + @Id + private Long id; + + @Embedded + private Root3 root3; + } + + @Embeddable + static class Root4 { + String root4Prop; + } + + @Embeddable + static class Mid4 extends Root4 { + } + + @Embeddable + static class Child4 extends Mid4 { + String child4Prop; + + @Embedded + Mid4 nested4; + } + + @Entity(name = "Entity4") + static class Entity4 { + @Id + private Long id; + + @Embedded + private Root4 root4; + } + + @Embeddable + static class EmbA { + String emb1; + + @Embedded + EmbB embB; + } + + @Embeddable + static class EmbB { + String embB; + + @Embedded + EmbA embA; + } + + @Entity(name = "Entity5") + static class Entity5 { + @Id + private Long id; + + @Embedded + private EmbA embA; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaUpdateAssociationSetNullValueTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaUpdateAssociationSetNullValueTest.java new file mode 100644 index 000000000000..aa4e2583f5da --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaUpdateAssociationSetNullValueTest.java @@ -0,0 +1,132 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.criteria; + +import jakarta.persistence.Basic; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.criteria.Root; +import org.hibernate.SessionFactory; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +@Jpa( + annotatedClasses = { + CriteriaUpdateAssociationSetNullValueTest.Parent.class, + CriteriaUpdateAssociationSetNullValueTest.Child.class} +) +@JiraKey("HHH-19085") +public class CriteriaUpdateAssociationSetNullValueTest { + + private static final Long PARENT_ID = 1L; + + @BeforeEach + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( + em -> { + em.persist( new Parent( PARENT_ID, "Lionello", new Child( 2L, "Andrea" ) ) ); + } + + ); + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().unwrap( SessionFactory.class ).getSchemaManager().truncateMappedObjects(); + } + + @Test + void testUpdateSetAssociationToNullValue(EntityManagerFactoryScope scope) { + scope.inTransaction( + em -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaUpdate update = cb.createCriteriaUpdate( Parent.class ); + Root msg = update.from( Parent.class ); + update.set( msg.get( "child" ), (Child) null ); + em.createQuery( update ).executeUpdate(); + } + ); + + scope.inTransaction( + em -> { + Parent parent = em.find( Parent.class, PARENT_ID ); + assertThat( parent ).isNotNull(); + assertThat( parent.getName() ).isNotNull(); + assertThat( parent.getChild() ).isNull(); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + @Basic + private String name; + + @ManyToOne(cascade = CascadeType.PERSIST) + private Child child; + + public Parent() { + } + + public Parent(Long id, String name, Child child) { + this.id = id; + this.name = name; + this.child = child; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Child getChild() { + return child; + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/metadata/Record.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/metadata/Record.java index aa08db8069a2..425f727775df 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/metadata/Record.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/metadata/Record.java @@ -25,9 +25,10 @@ public class Record { @Id @GeneratedValue String id; String text; - LocalDateTime timestamp = LocalDateTime.now(); + LocalDateTime timestamp; Record() {} - public Record(String text) { + public Record(String text, LocalDateTime timestamp) { this.text = text; + this.timestamp = timestamp; } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/metadata/TypesafeNamedQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/metadata/TypesafeNamedQueryTest.java index a27dcbff8b2a..c178d36cb5c0 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/metadata/TypesafeNamedQueryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/metadata/TypesafeNamedQueryTest.java @@ -4,27 +4,21 @@ */ package org.hibernate.orm.test.jpa.metadata; -import org.hibernate.community.dialect.FirebirdDialect; -import org.hibernate.community.dialect.InformixDialect; -import org.hibernate.dialect.SybaseASEDialect; import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; import org.hibernate.testing.orm.junit.Jpa; -import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.Test; +import java.time.LocalDate; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @Jpa(annotatedClasses = Record.class) public class TypesafeNamedQueryTest { - @SkipForDialect(dialectClass = SybaseASEDialect.class, reason = "'order by timestamp, id' not quite working") - @SkipForDialect(dialectClass = FirebirdDialect.class, reason = "'order by timestamp, id' not quite working") - @SkipForDialect(dialectClass = InformixDialect.class, reason = "'order by timestamp, id' not quite working") @Test void test(EntityManagerFactoryScope scope) { scope.inTransaction( entityManager -> { - Record record1 = new Record("Hello, World!"); - Record record2 = new Record("Goodbye!"); + Record record1 = new Record("Hello, World!", LocalDate.EPOCH.atStartOfDay()); + Record record2 = new Record("Goodbye!", LocalDate.EPOCH.atStartOfDay().plusSeconds( 1L )); entityManager.persist(record1); entityManager.persist(record2); String text = entityManager.createQuery(Record_._TextById_).setParameter(1, record1.id).getSingleResult(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CachedQueryShallowWithDiscriminatorBytecodeEnhancedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CachedQueryShallowWithDiscriminatorBytecodeEnhancedTest.java new file mode 100644 index 000000000000..7732ce8e535d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CachedQueryShallowWithDiscriminatorBytecodeEnhancedTest.java @@ -0,0 +1,163 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.query; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Id; +import jakarta.persistence.TypedQuery; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.CacheLayout; +import org.hibernate.annotations.QueryCacheLayout; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.stat.Statistics; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hibernate.jpa.HibernateHints.HINT_CACHEABLE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@JiraKey("HHH-19734") +@Jpa( + annotatedClasses = { + CachedQueryShallowWithDiscriminatorBytecodeEnhancedTest.Person.class + }, + generateStatistics = true, + properties = { + @Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true"), + @Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true") + } +) +@BytecodeEnhanced +public class CachedQueryShallowWithDiscriminatorBytecodeEnhancedTest { + + public final static String HQL = "select p from Person p"; + + @BeforeEach + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( + em -> { + Person person = new Person( 1L ); + person.setName( "Bob" ); + em.persist( person ); + } + ); + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().getSchemaManager().truncate(); + } + + @Test + public void testCacheableQuery(EntityManagerFactoryScope scope) { + + Statistics stats = getStatistics( scope ); + stats.clear(); + + // First time the query is executed, query and results are cached. + scope.inTransaction( + em -> { + loadPersons( em ); + + assertThatAnSQLQueryHasBeenExecuted( stats ); + + assertEquals( 0, stats.getQueryCacheHitCount() ); + assertEquals( 1, stats.getQueryCacheMissCount() ); + assertEquals( 1, stats.getQueryCachePutCount() ); + + assertEquals( 0, stats.getSecondLevelCacheHitCount() ); + assertEquals( 0, stats.getSecondLevelCacheMissCount() ); + assertEquals( 0, stats.getSecondLevelCachePutCount() ); + } + ); + + stats.clear(); + + // Second time the query is executed, list of entities are read from query cache + + scope.inTransaction( + em -> { + // Create a person proxy in the persistence context to trigger the HHH-19734 error + em.getReference( Person.class, 1L ); + + loadPersons( em ); + + assertThatNoSQLQueryHasBeenExecuted( stats ); + + assertEquals( 1, stats.getQueryCacheHitCount() ); + assertEquals( 0, stats.getQueryCacheMissCount() ); + assertEquals( 0, stats.getQueryCachePutCount() ); + + assertEquals( 1, stats.getSecondLevelCacheHitCount() ); + assertEquals( 0, stats.getSecondLevelCacheMissCount() ); + assertEquals( 0, stats.getSecondLevelCachePutCount() ); + } + ); + + } + + private static Statistics getStatistics(EntityManagerFactoryScope scope) { + return ((SessionFactoryImplementor) scope.getEntityManagerFactory()).getStatistics(); + } + + private static void loadPersons(EntityManager em) { + TypedQuery query = em.createQuery( HQL, Person.class ) + .setHint( HINT_CACHEABLE, true ); + Person person = query.getSingleResult(); + assertEquals( 1L, person.getId() ); + assertEquals( "Bob", person.getName() ); + } + + private static void assertThatAnSQLQueryHasBeenExecuted(Statistics stats) { + assertEquals( 1, stats.getQueryStatistics( HQL ).getExecutionCount() ); + } + + private static void assertThatNoSQLQueryHasBeenExecuted(Statistics stats) { + assertEquals( 0, stats.getQueryStatistics( HQL ).getExecutionCount() ); + } + + @Entity(name = "Person") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @QueryCacheLayout(layout = CacheLayout.SHALLOW) + public static class Person { + @Id + private Long id; + private String name; + + public Person() { + super(); + } + + public Person(Long id) { + this.id = id; + } + + 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; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CriteriaUpdateWithParametersTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CriteriaUpdateWithParametersTest.java index 40f665c57625..b7a97d81280b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CriteriaUpdateWithParametersTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CriteriaUpdateWithParametersTest.java @@ -4,81 +4,113 @@ */ package org.hibernate.orm.test.jpa.query; -import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; -import org.hibernate.testing.orm.junit.Jpa; -import org.junit.jupiter.api.Test; - import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import jakarta.persistence.Lob; import jakarta.persistence.Query; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.ParameterExpression; +import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Root; import jakarta.persistence.metamodel.EntityType; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; -@Jpa( - annotatedClasses = CriteriaUpdateWithParametersTest.Person.class -) + +@Jpa(annotatedClasses = { + CriteriaUpdateWithParametersTest.Person.class, + CriteriaUpdateWithParametersTest.Process.class +}) public class CriteriaUpdateWithParametersTest { @Test public void testCriteriaUpdate(EntityManagerFactoryScope scope) { - scope.inTransaction( - entityManager -> { - final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); - final CriteriaUpdate criteriaUpdate = criteriaBuilder.createCriteriaUpdate( Person.class ); - final Root root = criteriaUpdate.from( Person.class ); - - final ParameterExpression intValueParameter = criteriaBuilder.parameter( Integer.class ); - final ParameterExpression stringValueParameter = criteriaBuilder.parameter( String.class ); - - final EntityType personEntityType = entityManager.getMetamodel().entity( Person.class ); - - criteriaUpdate.set( - root.get( personEntityType.getSingularAttribute( "age", Integer.class ) ), - intValueParameter - ); - criteriaUpdate.where( criteriaBuilder.equal( - root.get( personEntityType.getSingularAttribute( "name", String.class ) ), - stringValueParameter - ) ); - - final Query query = entityManager.createQuery( criteriaUpdate ); - query.setParameter( intValueParameter, 9 ); - query.setParameter( stringValueParameter, "Luigi" ); - - query.executeUpdate(); - } - ); + scope.inTransaction( entityManager -> { + final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); + final CriteriaUpdate criteriaUpdate = criteriaBuilder.createCriteriaUpdate( Person.class ); + final Root root = criteriaUpdate.from( Person.class ); + + final ParameterExpression intValueParameter = criteriaBuilder.parameter( Integer.class ); + final ParameterExpression stringValueParameter = criteriaBuilder.parameter( String.class ); + + final EntityType personEntityType = entityManager.getMetamodel().entity( Person.class ); + + criteriaUpdate.set( root.get( personEntityType.getSingularAttribute( "age", Integer.class ) ), + intValueParameter ); + criteriaUpdate.where( + criteriaBuilder.equal( root.get( personEntityType.getSingularAttribute( "name", String.class ) ), + stringValueParameter ) ); + + final Query query = entityManager.createQuery( criteriaUpdate ); + query.setParameter( intValueParameter, 9 ); + query.setParameter( stringValueParameter, "Luigi" ); + + query.executeUpdate(); + } ); } @Test public void testCriteriaUpdate2(EntityManagerFactoryScope scope) { - scope.inTransaction( - entityManager -> { - final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); - final CriteriaUpdate criteriaUpdate = criteriaBuilder.createCriteriaUpdate( Person.class ); - final Root root = criteriaUpdate.from( Person.class ); + scope.inTransaction( entityManager -> { + final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); + final CriteriaUpdate criteriaUpdate = criteriaBuilder.createCriteriaUpdate( Person.class ); + final Root root = criteriaUpdate.from( Person.class ); - final ParameterExpression intValueParameter = criteriaBuilder.parameter( Integer.class ); - final ParameterExpression stringValueParameter = criteriaBuilder.parameter( String.class ); + final ParameterExpression intValueParameter = criteriaBuilder.parameter( Integer.class ); + final ParameterExpression stringValueParameter = criteriaBuilder.parameter( String.class ); - criteriaUpdate.set( "age", intValueParameter ); - criteriaUpdate.where( criteriaBuilder.equal( root.get( "name" ), stringValueParameter ) ); + criteriaUpdate.set( "age", intValueParameter ); + criteriaUpdate.where( criteriaBuilder.equal( root.get( "name" ), stringValueParameter ) ); - final Query query = entityManager.createQuery( criteriaUpdate ); - query.setParameter( intValueParameter, 9 ); - query.setParameter( stringValueParameter, "Luigi" ); + final Query query = entityManager.createQuery( criteriaUpdate ); + query.setParameter( intValueParameter, 9 ); + query.setParameter( stringValueParameter, "Luigi" ); - query.executeUpdate(); - } - ); + query.executeUpdate(); + } ); + } + + @Test + public void testCriteriaUpdate3(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + // test separate value-bind parameters + final CriteriaBuilder cb = em.getCriteriaBuilder(); + final CriteriaUpdate cu = cb.createCriteriaUpdate( Process.class ); + final Root root = cu.from( Process.class ); + cu.set( root.get( "name" ), (Object) null ); + cu.set( root.get( "payload" ), (Object) null ); + em.createQuery( cu ).executeUpdate(); + } ); + + scope.inTransaction( em -> { + // test with the same cb.value( null ) parameter instance + final HibernateCriteriaBuilder cb = (HibernateCriteriaBuilder) em.getCriteriaBuilder(); + final CriteriaUpdate cu = cb.createCriteriaUpdate( Process.class ); + final Root root = cu.from( Process.class ); + final Expression nullValue = cb.value( null ); + // a bit unfortunate, but we need to cast here to prevent ambiguous method references + final Path name = root.get( "name" ); + final Path payload = root.get( "payload" ); + final Expression nullString = cast( nullValue ); + final Expression nullBytes = cast( nullValue ); + cu.set( name, nullString ); + cu.set( payload, nullBytes ); + em.createQuery( cu ).executeUpdate(); + } ); + } + + private static Expression cast(Expression expression) { + //noinspection unchecked + return (Expression) expression; } @Entity(name = "Person") public static class Person { - @Id private String id; @@ -101,4 +133,18 @@ public Integer getAge() { return age; } } + + @Entity + public static class Process { + @Id + @GeneratedValue + private Long id; + + // All attributes below are necessary to reproduce the issue + + private String name; + + @Lob + private byte[] payload; + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/LockedRowsTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/LockedRowsTests.java index beb6d3b559bf..09936208ff1c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/LockedRowsTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/LockedRowsTests.java @@ -8,7 +8,7 @@ import jakarta.persistence.Timeout; import org.hibernate.PessimisticLockException; import org.hibernate.community.dialect.InformixDialect; -import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.lock.PessimisticEntityLockException; import org.hibernate.jpa.SpecHints; import org.hibernate.testing.orm.AsyncExecutor; @@ -87,6 +87,7 @@ void testFindNoWait(SessionFactoryScope factoryScope) { @Test @RequiresDialectFeature(feature = DialectFeatureChecks.SupportNoWait.class) @SkipForDialect(dialectClass = InformixDialect.class, reason = "no failure") + @SkipForDialect(dialectClass = CockroachDialect.class, reason = "Seems FOR UPDATE locks might block read accesses of other TXs") void testLockNoWait(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { session.find(Book.class,1, PESSIMISTIC_WRITE); @@ -105,10 +106,6 @@ void testLockNoWait(SessionFactoryScope factoryScope) { @Test @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsSkipLocked.class) - @SkipForDialect( - dialectClass = MariaDBDialect.class, - reason = "Cannot figure this out - it passes when run by itself, but fails when run as part of the complete suite." - ) void testQuerySkipLocked(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session1) -> { session1.find(Book.class,1, PESSIMISTIC_WRITE); @@ -127,10 +124,6 @@ void testQuerySkipLocked(SessionFactoryScope factoryScope) { @Test @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsSkipLocked.class) - @SkipForDialect( - dialectClass = MariaDBDialect.class, - reason = "Cannot figure this out - it passes when run by itself, but fails when run as part of the complete suite." - ) @SkipForDialect(dialectClass = InformixDialect.class, reason = "no failure") void testFindSkipLocked(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/ConverterOverrideTypeRegisttrationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/ConverterOverrideTypeRegisttrationTest.java new file mode 100644 index 000000000000..4718a5007f9f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/ConverterOverrideTypeRegisttrationTest.java @@ -0,0 +1,102 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.converted.converter; + +import java.util.BitSet; + +import org.hibernate.annotations.TypeRegistration; +import org.hibernate.orm.test.mapping.basic.bitset.BitSetHelper; +import org.hibernate.orm.test.mapping.basic.bitset.BitSetUserType; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + *
+ * The @Converter should take precedence over @TypeRegistration.
+ * This test shows that this is not the case.
+ *
+ * To ensure that the @Converter is taken into account without @TypeRegistration, you just need to remove the @TypeRegistration.
+ * 
+ * + * @author Vincent Bouthinon + */ +@DomainModel( + annotatedClasses = { + ConverterOverrideTypeRegisttrationTest.SimpleEntity.class + } +) +@SessionFactory +@JiraKey(value = "HHH-19589") +public class ConverterOverrideTypeRegisttrationTest { + + @Test + void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final SimpleEntity object = new SimpleEntity( 77L ); + BitSet bitSet = new BitSet(); + bitSet.set( 0, true ); + object.setBitSet( bitSet ); + session.persist( object ); + session.flush(); + session.clear(); + SimpleEntity simpleEntity = session.find( SimpleEntity.class, object.id ); + assertThat( simpleEntity.getBitSet().get( 7 ) ).isTrue(); + } ); + } + + + @Entity(name = "SimpleEntity") + @TypeRegistration(basicClass = BitSet.class, userType = BitSetUserType.class) // Remove this annotation to test the use of @Converter + public static class SimpleEntity { + + @Id + private Long id; + @Convert(converter = BitSetConverter.class) + private BitSet bitSet; + + public SimpleEntity() { + } + + public SimpleEntity(Long id) { + this.id = id; + } + + public BitSet getBitSet() { + return bitSet; + } + + public void setBitSet(final BitSet bitSet) { + this.bitSet = bitSet; + } + } + + @Converter + public static class BitSetConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(final BitSet attribute) { + return BitSetHelper.bitSetToString( attribute ); + } + + @Override + public BitSet convertToEntityAttribute(final String dbData) { + BitSet bitSet = new BitSet(); + bitSet.set( 7, true ); + return bitSet; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteManyToManyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteManyToManyTest.java index fc383d32ea76..fd294482511b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteManyToManyTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteManyToManyTest.java @@ -88,5 +88,8 @@ static class A { static class B { @Id long id; + @ManyToMany(mappedBy = "bs") + @OnDelete(action = OnDeleteAction.CASCADE) + Set as = new HashSet<>(); } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryShadowingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryShadowingTest.java new file mode 100644 index 000000000000..d4e266a82b0e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryShadowingTest.java @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + + +@DomainModel( + annotatedClasses = { + SubQueryShadowingTest.TestEntity.class, + } +) +@SessionFactory +public class SubQueryShadowingTest { + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsOffsetInSubquery.class, comment = "The check is for both, limit and offset in subqueries") + @Jira("https://hibernate.atlassian.net/browse/HHH-19745") + public void testSelectCase(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createQuery( + """ + from TestEntity t + left join TestEntity t2 on exists ( + select 1 + from TestEntity t + order by t.id + limit 1 + ) + """, TestEntity.class ) + .list(); + } ); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + + @Id + @GeneratedValue + private Long id; + + private String name; + + public TestEntity() { + } + + public TestEntity(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/BasicEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/BasicEntity.java index 0e5e9a2f4556..718da0242b1d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/BasicEntity.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/BasicEntity.java @@ -22,4 +22,36 @@ public class BasicEntity { @ManyToOne OtherEntity other; + + 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; + } + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } + + public OtherEntity getOther() { + return other; + } + + public void setOther(OtherEntity other) { + this.other = other; + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/SpecificationReuseTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/SpecificationReuseTest.java new file mode 100644 index 000000000000..ebe1b8bd9992 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/SpecificationReuseTest.java @@ -0,0 +1,175 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.dynamic; + +import java.util.function.Consumer; + +import org.hibernate.query.Order; +import org.hibernate.query.restriction.Restriction; +import org.hibernate.query.specification.MutationSpecification; +import org.hibernate.query.specification.SelectionSpecification; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + BasicEntity.class, + OtherEntity.class, +}) +@SessionFactory(useCollectingStatementInspector = true) +@Jira( "https://hibernate.atlassian.net/browse/HHH-19781" ) +public class SpecificationReuseTest { + @Test + public void dynamicSelect(SessionFactoryScope scope) { + final var inspector = scope.getCollectingStatementInspector(); + scope.inTransaction( session -> { + final var spec = SelectionSpecification.create( BasicEntity.class ) + .sort( Order.asc( BasicEntity_.position ) ) + .restrict( Restriction.like( BasicEntity_.name, "entity_%" ) ); + nTimes( spec, 3, s -> { + inspector.clear(); + assertThat( s.createQuery( session ).list() ).extracting( BasicEntity::getId ).containsExactly( 2, 1 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "position", 2 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "name", 2 ); + } ); + } ); + } + + @Test + public void hqlSelect(SessionFactoryScope scope) { + final var inspector = scope.getCollectingStatementInspector(); + scope.inTransaction( session -> { + final var spec = SelectionSpecification.create( BasicEntity.class, "from BasicEntity" ) + .sort( Order.asc( BasicEntity_.position ) ) + .restrict( Restriction.like( BasicEntity_.name, "entity_%" ) ); + nTimes( spec, 3, s -> { + inspector.clear(); + assertThat( s.createQuery( session ).list() ).extracting( BasicEntity::getId ).containsExactly( 2, 1 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "position", 2 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "name", 2 ); + } ); + } ); + } + + @Test + public void criteriaSelect(SessionFactoryScope scope) { + final var inspector = scope.getCollectingStatementInspector(); + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var query = cb.createQuery( BasicEntity.class ); + final var root = query.from( BasicEntity.class ); + final var spec = SelectionSpecification.create( query.select( root ) ) + .sort( Order.asc( BasicEntity_.position ) ) + .restrict( Restriction.like( BasicEntity_.name, "entity_%" ) ); + nTimes( spec, 3, s -> { + inspector.clear(); + assertThat( s.createQuery( session ).list() ).extracting( BasicEntity::getId ).containsExactly( 2, 1 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "position", 2 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "name", 2 ); + } ); + } ); + } + + @Test + public void hqlMutation(SessionFactoryScope scope) { + final var inspector = scope.getCollectingStatementInspector(); + scope.inTransaction( session -> { + final var e = new BasicEntity(); + e.setId( 33 ); + session.persist( e ); + } ); + scope.inTransaction( session -> { + final var spec = MutationSpecification.create( BasicEntity.class, "update BasicEntity set name = 'entity_33'" ) + .restrict( Restriction.equal( BasicEntity_.id, 33 ) ); + nTimes( spec, 3, s -> { + inspector.clear(); + assertThat( s.createQuery( session ).executeUpdate() ).isEqualTo( 1 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "id", 1 ); + } ); + } ); + scope.inTransaction( session -> { + final var spec = MutationSpecification.create( BasicEntity.class, "delete BasicEntity" ) + .restrict( Restriction.equal( BasicEntity_.name, "entity_33" ) ); + nTimes( spec, 3, s -> { + inspector.clear(); + s.createQuery( session ).executeUpdate(); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "name", 1 ); + } ); + } ); + } + + @Test + public void criteriaMutation(SessionFactoryScope scope) { + final var inspector = scope.getCollectingStatementInspector(); + scope.inTransaction( session -> { + final var e = new BasicEntity(); + e.setId( 44 ); + session.persist( e ); + } ); + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var cu = cb.createCriteriaUpdate( BasicEntity.class ); + final var spec = MutationSpecification.create( cu.set( BasicEntity_.name, "entity_44" ) ) + .restrict( Restriction.equal( BasicEntity_.id, 44 ) ); + nTimes( spec, 3, s -> { + inspector.clear(); + assertThat( s.createQuery( session ).executeUpdate() ).isEqualTo( 1 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "id", 1 ); + } ); + } ); + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var cd = cb.createCriteriaDelete( BasicEntity.class ); + final var spec = MutationSpecification.create( cd ) + .restrict( Restriction.equal( BasicEntity_.name, "entity_44" ) ); + nTimes( spec, 3, s -> { + inspector.clear(); + s.createQuery( session ).executeUpdate(); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "name", 1 ); + } ); + } ); + } + + void nTimes(SelectionSpecification spec, int n, Consumer> consumer) { + for ( int i = 0; i < n; i++ ) { + consumer.accept( spec ); + } + } + + void nTimes(MutationSpecification spec, int n, Consumer> consumer) { + for ( int i = 0; i < n; i++ ) { + consumer.accept( spec ); + } + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var e1 = new BasicEntity(); + e1.setId( 1 ); + e1.setName( "entity_1" ); + e1.setPosition( 99 ); + session.persist( e1 ); + + final var e2 = new BasicEntity(); + e2.setId( 2 ); + e2.setName( "entity_2" ); + e2.setPosition( 42 ); + session.persist( e2 ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/mutationquery/MutationQueriesCollectionTableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/mutationquery/MutationQueriesCollectionTableTest.java new file mode 100644 index 000000000000..69f884d993ad --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/mutationquery/MutationQueriesCollectionTableTest.java @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.mutationquery; + +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; + +@SessionFactory +@DomainModel(annotatedClasses = { + MutationQueriesCollectionTableTest.Base1.class, + MutationQueriesCollectionTableTest.Table1.class +}) +@Jira("https://hibernate.atlassian.net/browse/HHH-19740") +public class MutationQueriesCollectionTableTest { + + @Test + public void testDelete(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createQuery( "delete from Table1 where name = :name" ) + .setParameter( "name", "test" ) + .executeUpdate(); + } ); + } + + @Entity(name = "Base1") + @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) + public abstract static class Base1 { + + @Id + private Long id; + + @SuppressWarnings("unused") + private String name; + + @ElementCollection + private List roles; + + } + + @Entity(name = "Table1") + @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) + public static class Table1 extends Base1 { + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderColumnDeduplicationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderColumnDeduplicationTest.java new file mode 100644 index 000000000000..0e327fbc9f22 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderColumnDeduplicationTest.java @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.sql; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +@DomainModel(annotatedClasses = { + NativeQueryResultBuilderColumnDeduplicationTest.MyEntity.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19712") +public class NativeQueryResultBuilderColumnDeduplicationTest { + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createNativeQuery( "select {t.*} from MyEntity t", Object.class ) + .addEntity( "t", MyEntity.class ) + .getResultList(); + } + ); + } + + @Entity(name = "MyEntity") + public static class MyEntity { + @EmbeddedId + private MyEntityPk id; + @Column(insertable = false, updatable = false) + private String name; + private String description; + } + + @Embeddable + public static class MyEntityPk { + private String id; + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sqm/SelfRenderingSqmFunctionWithoutArgumentsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sqm/SelfRenderingSqmFunctionWithoutArgumentsTest.java new file mode 100644 index 000000000000..f3ddd59178b1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sqm/SelfRenderingSqmFunctionWithoutArgumentsTest.java @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.sqm; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportPartitionBy; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +@DomainModel( + annotatedClasses = SelfRenderingSqmFunctionWithoutArgumentsTest.Dummy.class +) +@SessionFactory +@JiraKey("HHH-19719") +@RequiresDialectFeature(feature = SupportPartitionBy.class) +public class SelfRenderingSqmFunctionWithoutArgumentsTest { + + @BeforeAll + static void init(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new Dummy(1, "John Doe") ); + session.persist( new Dummy(2, "Dave Default") ); + } ); + } + + @Test + void test(SessionFactoryScope scope) { + scope.inSession( session -> { + session.createQuery( """ + with tmp as ( + select id id, name name, row_number() over (order by name) pos + from Dummy + ) + select id, name, pos from tmp + """ ).getResultList(); + } ); + + } + + @Entity(name = "Dummy") + static class Dummy { + @Id + private Integer id; + + private String name; + + private Dummy() { + // for use by Hibernate + } + + public Dummy(Integer id, String name) { + this.id = id; + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/CollectionListenerInStatelessSessionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/CollectionListenerInStatelessSessionTest.java index 7d322d50ac31..192b6963cf5d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/CollectionListenerInStatelessSessionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/CollectionListenerInStatelessSessionTest.java @@ -18,6 +18,7 @@ import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @DomainModel(annotatedClasses = {EntityA.class, EntityB.class}) @@ -64,6 +65,8 @@ class MyPreCollectionRecreateEventListener implements PreCollectionRecreateEvent @Override public void onPreRecreateCollection(PreCollectionRecreateEvent event) { + assertThat( event.getAffectedOwnerOrNull() ).isNotNull(); + assertThat( event.getCollection().getOwner() ).isNotNull(); called++; } @@ -75,6 +78,8 @@ class MyPreCollectionRemoveEventListener implements PreCollectionRemoveEventList @Override public void onPreRemoveCollection(PreCollectionRemoveEvent event) { + assertThat( event.getAffectedOwnerOrNull() ).isNotNull(); + assertThat( event.getCollection().getOwner() ).isNotNull(); called++; } @@ -86,6 +91,8 @@ class MyPostCollectionRecreateEventListener implements PostCollectionRecreateEve @Override public void onPostRecreateCollection(PostCollectionRecreateEvent event) { + assertThat( event.getAffectedOwnerOrNull() ).isNotNull(); + assertThat( event.getCollection().getOwner() ).isNotNull(); called++; } @@ -97,6 +104,8 @@ class MyPostCollectionRemoveEventListener implements PostCollectionRemoveEventLi @Override public void onPostRemoveCollection(PostCollectionRemoveEvent event) { + assertThat( event.getAffectedOwnerOrNull() ).isNotNull(); + assertThat( event.getCollection().getOwner() ).isNotNull(); called++; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/typeoverride/TypeOverrideTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/typeoverride/TypeOverrideTest.java index c7ddbfd7558f..8cf0e3691f79 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/typeoverride/TypeOverrideTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/typeoverride/TypeOverrideTest.java @@ -15,7 +15,6 @@ import org.hibernate.dialect.SybaseDialect; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.IntegerJdbcType; -import org.hibernate.type.descriptor.jdbc.VarbinaryJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.testing.orm.junit.BaseSessionFactoryFunctionalTest; @@ -54,7 +53,7 @@ public void testStandardBasicSqlTypeDescriptor() { // A few dialects explicitly override BlobTypeDescriptor.DEFAULT if ( CockroachDialect.class.isInstance( dialect ) ) { assertSame( - VarbinaryJdbcType.INSTANCE, + BlobJdbcType.MATERIALIZED, jdbcTypeRegistry.getDescriptor( Types.BLOB ) ); } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/id/AbstractCompositeIdMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/id/AbstractCompositeIdMapper.java index dbcd7b355613..fb6f1540d186 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/id/AbstractCompositeIdMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/id/AbstractCompositeIdMapper.java @@ -9,8 +9,10 @@ import org.hibernate.envers.exception.AuditException; import org.hibernate.envers.internal.entities.PropertyData; import org.hibernate.envers.internal.tools.Tools; -import org.hibernate.internal.util.ReflectHelper; +import org.hibernate.mapping.Component; import org.hibernate.service.ServiceRegistry; +import org.hibernate.type.spi.CompositeTypeImplementor; + /** * An abstract identifier mapper implementation specific for composite identifiers. @@ -20,13 +22,16 @@ * @author Chris Cranford */ public abstract class AbstractCompositeIdMapper extends AbstractIdMapper implements SimpleIdMapperBuilder { - protected final Class compositeIdClass; - + protected final CompositeTypeImplementor compositeType; protected Map ids; - protected AbstractCompositeIdMapper(Class compositeIdClass, ServiceRegistry serviceRegistry) { + protected AbstractCompositeIdMapper(Component component) { + this( component.getServiceRegistry(), (CompositeTypeImplementor) component.getType() ); + } + + protected AbstractCompositeIdMapper(ServiceRegistry serviceRegistry, CompositeTypeImplementor compositeType) { super( serviceRegistry ); - this.compositeIdClass = compositeIdClass; + this.compositeType = compositeType; ids = Tools.newLinkedHashMap(); } @@ -46,24 +51,44 @@ public Object mapToIdFromMap(Map data) { return null; } - final Object compositeId = instantiateCompositeId(); - for ( AbstractIdMapper mapper : ids.values() ) { - if ( !mapper.mapToEntityFromMap( compositeId, data ) ) { - return null; + if ( !compositeType.isMutable() ) { + return mapToImmutableIdFromMap( data ); + } + + final Object compositeId = instantiateCompositeId( null ); + + if ( compositeType.isMutable() ) { + for ( AbstractIdMapper mapper : ids.values() ) { + if ( !mapper.mapToEntityFromMap( compositeId, data ) ) { + return null; + } } } return compositeId; } + protected Object mapToImmutableIdFromMap(Map data) { + final var propertyNames = compositeType.getPropertyNames(); + final var values = new Object[propertyNames.length]; + for ( int i = 0; i < propertyNames.length; i++ ) { + values[i] = data.get( propertyNames[i] ); + } + return instantiateCompositeId( values ); + } + @Override public void mapToEntityFromEntity(Object objectTo, Object objectFrom) { // no-op; does nothing } - protected Object instantiateCompositeId() { + protected Object instantiateCompositeId(Object[] values) { try { - return ReflectHelper.getDefaultConstructor( compositeIdClass ).newInstance(); + return compositeType.getMappingModelPart() + .getEmbeddableTypeDescriptor() + .getRepresentationStrategy() + .getInstantiator() + .instantiate( () -> values ); } catch ( Exception e ) { throw new AuditException( e ); diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/id/EmbeddedIdMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/id/EmbeddedIdMapper.java index f4968688bf95..4819a71732a6 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/id/EmbeddedIdMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/id/EmbeddedIdMapper.java @@ -15,6 +15,7 @@ import org.hibernate.mapping.Component; import org.hibernate.property.access.spi.Setter; import org.hibernate.service.ServiceRegistry; +import org.hibernate.type.spi.CompositeTypeImplementor; /** * An identifier mapper implementation for {@link jakarta.persistence.EmbeddedId} mappings. @@ -26,12 +27,12 @@ public class EmbeddedIdMapper extends AbstractCompositeIdMapper implements Simpl private PropertyData idPropertyData; public EmbeddedIdMapper(PropertyData propertyData, Component component) { - super( component.getComponentClass(), component.getServiceRegistry() ); + super( component ); this.idPropertyData = propertyData; } - private EmbeddedIdMapper(PropertyData idPropertyData, Class compositeIdClass, ServiceRegistry serviceRegistry) { - super( compositeIdClass, serviceRegistry ); + private EmbeddedIdMapper(PropertyData idPropertyData, CompositeTypeImplementor compositeType, ServiceRegistry serviceRegistry) { + super( serviceRegistry, compositeType ); this.idPropertyData = idPropertyData; } @@ -58,11 +59,17 @@ public boolean mapToEntityFromMap(final Object obj, final Map data) { final Setter setter = ReflectionTools.getSetter( obj.getClass(), idPropertyData, getServiceRegistry() ); try { - final Object subObj = instantiateCompositeId(); - + final Object subObj; boolean ret = true; - for ( IdMapper idMapper : ids.values() ) { - ret &= idMapper.mapToEntityFromMap( subObj, data ); + if ( compositeType.isMutable() ) { + subObj = instantiateCompositeId( null ); + for ( IdMapper idMapper : ids.values() ) { + ret &= idMapper.mapToEntityFromMap( subObj, data ); + } + } + else { + subObj = mapToImmutableIdFromMap( data ); + ret = subObj != null; } if ( ret ) { @@ -78,7 +85,7 @@ public boolean mapToEntityFromMap(final Object obj, final Map data) { @Override public IdMapper prefixMappedProperties(String prefix) { - final EmbeddedIdMapper ret = new EmbeddedIdMapper( idPropertyData, compositeIdClass, getServiceRegistry() ); + final EmbeddedIdMapper ret = new EmbeddedIdMapper( idPropertyData, compositeType, getServiceRegistry() ); for ( PropertyData propertyData : ids.keySet() ) { final String propertyName = propertyData.getName(); diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/id/MultipleIdMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/id/MultipleIdMapper.java index 5cdc253414a4..426c8a970160 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/id/MultipleIdMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/id/MultipleIdMapper.java @@ -15,6 +15,7 @@ import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; import org.hibernate.service.ServiceRegistry; +import org.hibernate.type.spi.CompositeTypeImplementor; /** * An implementation of an identifier mapper for {@link jakarta.persistence.IdClass} or multiple @@ -24,17 +25,12 @@ * @author Chris Cranford */ public class MultipleIdMapper extends AbstractCompositeIdMapper implements SimpleIdMapperBuilder { - - private final boolean embedded; - public MultipleIdMapper(Component component) { - super( component.getComponentClass(), component.getServiceRegistry() ); - this.embedded = component.isEmbedded(); + super( component ); } - private MultipleIdMapper(boolean embedded, Class compositeIdClass, ServiceRegistry serviceRegistry) { - super( compositeIdClass, serviceRegistry ); - this.embedded = embedded; + private MultipleIdMapper(CompositeTypeImplementor compositeType, ServiceRegistry serviceRegistry) { + super( serviceRegistry, compositeType ); } @Override @@ -44,8 +40,8 @@ public void add(PropertyData propertyData) { @Override public void mapToMapFromId(Session session, Map data, Object obj) { - if ( compositeIdClass.isInstance( obj ) ) { - if ( embedded ) { + if ( compositeType.getReturnedClass().isInstance( obj ) ) { + if ( compositeType.isEmbedded() ) { final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( obj ); if ( lazyInitializer != null ) { obj = lazyInitializer.getInternalIdentifier(); @@ -77,7 +73,7 @@ public void mapToMapFromId(Map data, Object obj) { @Override public void mapToMapFromEntity(Map data, Object obj) { - if ( embedded ) { + if ( compositeType.isEmbedded() ) { final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( obj ); if ( lazyInitializer != null ) { obj = lazyInitializer.getInternalIdentifier(); @@ -100,7 +96,7 @@ public boolean mapToEntityFromMap(Object obj, Map data) { @Override public IdMapper prefixMappedProperties(String prefix) { - final MultipleIdMapper ret = new MultipleIdMapper( embedded, compositeIdClass, getServiceRegistry() ); + final MultipleIdMapper ret = new MultipleIdMapper( compositeType, getServiceRegistry() ); for ( PropertyData propertyData : ids.keySet() ) { final String propertyName = propertyData.getName(); @@ -116,7 +112,7 @@ public Object mapToIdFromEntity(Object data) { return null; } - final Object compositeId = instantiateCompositeId(); + final Object compositeId = instantiateCompositeId( null ); for ( AbstractIdMapper mapper : ids.values() ) { mapper.mapToEntityFromEntity( compositeId, data ); } diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/query/ids/RecordIdRelatedIdQueryTest.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/query/ids/RecordIdRelatedIdQueryTest.java new file mode 100644 index 000000000000..bdd76747fee3 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/query/ids/RecordIdRelatedIdQueryTest.java @@ -0,0 +1,251 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.envers.test.integration.query.ids; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.ManyToOne; + +import org.hibernate.annotations.processing.Exclude; +import org.hibernate.envers.Audited; +import org.hibernate.envers.RevisionType; +import org.hibernate.envers.query.AuditEntity; +import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase; +import org.hibernate.orm.test.envers.Priority; +import org.hibernate.testing.orm.junit.Jira; +import org.junit.Test; + +import org.hibernate.testing.transaction.TransactionUtil; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +@Exclude +@Jira("https://hibernate.atlassian.net/browse/HHH-19393") +public class RecordIdRelatedIdQueryTest extends BaseEnversJPAFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Person.class, Document.class, PersonDocument.class }; + } + + @Test + @Priority(10) + public void initData() { + TransactionUtil.doInJPA( this::entityManagerFactory, entityManager -> { + final Person person = new Person( 1, "Chris" ); + final Document document = new Document( 1, "DL" ); + final PersonDocument pd = new PersonDocument( person, document ); + entityManager.persist( person ); + entityManager.persist( document ); + entityManager.persist( pd ); + } ); + + TransactionUtil.doInJPA( this::entityManagerFactory, entityManager -> { + final Person person = entityManager.find( Person.class, 1 ); + final Document document = new Document( 2, "Passport" ); + final PersonDocument pd = new PersonDocument( person, document ); + entityManager.persist( document ); + entityManager.persist( pd ); + } ); + + TransactionUtil.doInJPA( this::entityManagerFactory, entityManager -> { + final Person person = entityManager.find( Person.class, 1 ); + final Document document = entityManager.find( Document.class, 1 ); + final PersonDocument pd = entityManager + .createQuery( "FROM PersonDocument WHERE person.id = :person AND document.id = :document", PersonDocument.class ) + .setParameter( "person", person.getId() ) + .setParameter( "document", document.getId() ) + .getSingleResult(); + + entityManager.remove( pd ); + entityManager.remove( document ); + } ); + } + + @Test + public void testRevisionCounts() { + assertEquals( Arrays.asList( 1 ), getAuditReader().getRevisions( Person.class, 1 ) ); + assertEquals( Arrays.asList( 1, 3 ), getAuditReader().getRevisions( Document.class, 1 ) ); + assertEquals( Arrays.asList( 2 ), getAuditReader().getRevisions( Document.class, 2 ) ); + } + + @Test + public void testRelatedIdQueries() { + TransactionUtil.doInJPA( this::entityManagerFactory, entityManager -> { + List results = getAuditReader().createQuery().forRevisionsOfEntity( PersonDocument.class, false, true ) + .add( AuditEntity.relatedId( "person" ).eq( 1 ) ) + .add( AuditEntity.revisionNumber().eq( 1 ) ) + .getResultList(); + assertEquals( 1, results.size() ); + final Document document = ( (PersonDocument) ( (Object[]) results.get( 0 ) )[0] ).getDocument(); + assertEquals( "DL", document.getName() ); + } ); + + TransactionUtil.doInJPA( this::entityManagerFactory, entityManager -> { + List results = getAuditReader().createQuery().forRevisionsOfEntity( PersonDocument.class, false, true ) + .add( AuditEntity.relatedId( "person" ).eq( 1 ) ) + .add( AuditEntity.revisionNumber().eq( 2 ) ) + .getResultList(); + assertEquals( 1, results.size() ); + final Document document = ( (PersonDocument) ( (Object[]) results.get( 0 ) )[0] ).getDocument(); + assertEquals( "Passport", document.getName() ); + } ); + + TransactionUtil.doInJPA( this::entityManagerFactory, entityManager -> { + List results = getAuditReader().createQuery().forRevisionsOfEntity( PersonDocument.class, false, true ) + .add( AuditEntity.relatedId( "person" ).eq( 1 ) ) + .add( AuditEntity.revisionNumber().eq( 3 ) ) + .getResultList(); + assertEquals( 1, results.size() ); + final Document document = ( (PersonDocument) ( (Object[]) results.get( 0 ) )[0] ).getDocument(); + assertNull( document.getName() ); + } ); + + TransactionUtil.doInJPA( this::entityManagerFactory, entityManager -> { + List results = getAuditReader().createQuery().forRevisionsOfEntity( PersonDocument.class, false, true ) + .add( AuditEntity.relatedId( "document" ).eq( 1 ) ) + .getResultList(); + assertEquals( 2, results.size() ); + for ( Object result : results ) { + Object[] row = (Object[]) result; + final RevisionType revisionType = (RevisionType) row[2]; + final Document document = ( (PersonDocument) row[0] ).getDocument(); + if ( RevisionType.ADD.equals( revisionType ) ) { + assertEquals( "DL", document.getName() ); + } + else if ( RevisionType.DEL.equals( revisionType ) ) { + assertNull( document.getName() ); + } + } + } ); + + TransactionUtil.doInJPA( this::entityManagerFactory, entityManager -> { + List results = getAuditReader().createQuery().forRevisionsOfEntity( PersonDocument.class, false, true ) + .add( AuditEntity.relatedId( "document" ).eq( 2 ) ) + .getResultList(); + assertEquals( 1, results.size() ); + for ( Object result : results ) { + Object[] row = (Object[]) result; + final RevisionType revisionType = (RevisionType) row[2]; + final Document document = ( (PersonDocument) row[0] ).getDocument(); + assertEquals( RevisionType.ADD, revisionType ); + assertEquals( "Passport", document.getName() ); + } + } ); + } + + @Audited + @Entity(name = "PersonDocument") + @IdClass( PersonDocumentId.class ) + public static class PersonDocument implements Serializable { + @Id + @ManyToOne(optional = false) + private Document document; + + @Id + @ManyToOne(optional = false) + private Person person; + + PersonDocument() { + + } + + PersonDocument(Person person, Document document) { + this.document = document; + this.person = person; + } + + public Document getDocument() { + return document; + } + + public void setDocument(Document document) { + this.document = document; + } + + public Person getPerson() { + return person; + } + + public void setPerson(Person person) { + this.person = person; + } + } + + public record PersonDocumentId(Document document, Person person) { + } + + @Audited + @Entity(name = "Document") + public static class Document { + @Id + private Integer id; + private String name; + + Document() { + + } + + Document(Integer id, String name) { + this.id = id; + 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; + } + } + + @Entity(name = "Person") + @Audited + public static class Person { + @Id + private Integer id; + private String name; + + Person() { + + } + + Person(Integer id, String name) { + this.id = id; + 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; + } + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/blob/BasicBlobTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/blob/BasicBlobTest.java index 74d7ead88bd2..02baf048950e 100644 --- a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/blob/BasicBlobTest.java +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/blob/BasicBlobTest.java @@ -89,6 +89,7 @@ public void testGenerateProxyStream() throws URISyntaxException { .getResource( "org/hibernate/orm/test/envers/integration/blob/blob.txt" ).toURI() ); try (final InputStream stream = new BufferedInputStream( Files.newInputStream( path ) )) { + final long length = Files.size( path ); doInJPA( this::entityManagerFactory, entityManager -> { final Asset asset = new Asset(); asset.setFileName( "blob.txt" ); @@ -108,7 +109,7 @@ public void testGenerateProxyStream() throws URISyntaxException { // H2, MySQL, Oracle, SQL Server work this way. // // - Blob blob = BlobProxy.generateProxy( stream, 9192L ); + Blob blob = BlobProxy.generateProxy( stream, length ); asset.setData( blob ); entityManager.persist( asset ); diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/mapping/enumeratedvalue/CharEnumerateValueTests.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/mapping/enumeratedvalue/CharEnumerateValueTests.java new file mode 100644 index 000000000000..98d87b972ea8 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/mapping/enumeratedvalue/CharEnumerateValueTests.java @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.envers.mapping.enumeratedvalue; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.EnumeratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.envers.Audited; +import org.hibernate.testing.orm.junit.EntityManagerFactoryBasedFunctionalTest; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.type.SqlTypes; +import org.junit.Test; + + +@JiraKey( "HHH-19747" ) +public class CharEnumerateValueTests extends EntityManagerFactoryBasedFunctionalTest { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] {Person.class}; + } + + @Test + public void testBasicUsage() { + final EntityManagerFactory testEmf = produceEntityManagerFactory(); + testEmf.close(); + } + + public enum Gender { + MALE( 'M' ), + FEMALE( 'F' ), + OTHER( 'U' ); + + @EnumeratedValue + private final char code; + + Gender(char code) { + this.code = code; + } + + public char getCode() { + return code; + } + } + + @Audited + @Entity(name = "Person") + @Table(name = "persons") + public static class Person { + @Id + private Integer id; + private String name; + @Enumerated(EnumType.STRING) + @JdbcTypeCode(SqlTypes.CHAR) + @Column(length = 1) + private Gender gender; + + public Person() { + } + + public Person(Integer id, String name, Gender gender) { + this.id = id; + this.name = name; + this.gender = gender; + } + } +} diff --git a/hibernate-platform/hibernate-platform.gradle b/hibernate-platform/hibernate-platform.gradle index 16998e853f59..d5b4e094419d 100644 --- a/hibernate-platform/hibernate-platform.gradle +++ b/hibernate-platform/hibernate-platform.gradle @@ -36,6 +36,7 @@ dependencies { api jakartaLibs.jpa api jakartaLibs.jta + api jakartaLibs.data runtime libs.antlrRuntime runtime libs.logging diff --git a/hibernate-testing/hibernate-testing.gradle b/hibernate-testing/hibernate-testing.gradle index f64f1e44418c..c80d85417513 100644 --- a/hibernate-testing/hibernate-testing.gradle +++ b/hibernate-testing/hibernate-testing.gradle @@ -41,7 +41,20 @@ dependencies { implementation libs.hibernateModels implementation libs.jandex - implementation testLibs.wildFlyTxnClient + implementation(libs.logging) + // See https://hibernate.atlassian.net/browse/HHH-19743 + // While this dependency isn't explicitly used, removing it will lead to using JBossStandAloneJtaPlatform + // by default in tests, which will apparently lead to leaks detected by HibernateClassLoaderLeaksTest, + // which will fail. + // JBossStandAloneJtaPlatform is old and likely wouldn't work with Jakarta EE anyway, + // so I won't try to fix it, but we might want to remove it in the next minor, + // and then we can remove the dependency below. + implementation(testLibs.wildFlyTxnClient) { + // WildFly Elytron includes a whole copy of JBoss Logging, Jackson with its original package, + // not even shaded, which causes lots of problems. + // Since we don't need WildFly Elytron in our tests, we'll just exclude it. + exclude group: 'org.wildfly.security', module: 'wildfly-elytron' + } implementation testLibs.junit5Engine implementation testLibs.junit5Launcher diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/jpa/PersistenceUnitInfoImpl.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/jpa/PersistenceUnitInfoImpl.java index bf73272aaec3..b5c6427c8611 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/jpa/PersistenceUnitInfoImpl.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/jpa/PersistenceUnitInfoImpl.java @@ -44,6 +44,7 @@ public class PersistenceUnitInfoImpl implements PersistenceUnitInfo { private List mappingFiles; private List managedClassNames; private boolean excludeUnlistedClasses; + private ClassLoader classLoader; public PersistenceUnitInfoImpl(String name) { this.name = name; @@ -142,6 +143,15 @@ public void setExcludeUnlistedClasses(boolean excludeUnlistedClasses) { this.excludeUnlistedClasses = excludeUnlistedClasses; } + @Override + public ClassLoader getClassLoader() { + return classLoader; + } + + public void setClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + @Override public String getPersistenceXMLSchemaVersion() { return null; @@ -167,11 +177,6 @@ public URL getPersistenceUnitRootUrl() { return null; } - @Override - public ClassLoader getClassLoader() { - return null; - } - @Override public void addTransformer(ClassTransformer transformer) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 7bb583f3801f..9476ef9707b3 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -453,7 +453,6 @@ public boolean apply(Dialect dialect) { public static class SupportsCteInsertStrategy implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof PostgreSQLDialect - || dialect instanceof CockroachDialect || dialect instanceof DB2Dialect || dialect instanceof GaussDBDialect; } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFilterExtension.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFilterExtension.java index 92856ec81f82..816617c35527 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFilterExtension.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFilterExtension.java @@ -8,8 +8,10 @@ import java.util.LinkedHashMap; import java.util.Locale; +import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.SimpleDatabaseVersion; import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExtensionContext; @@ -181,27 +183,46 @@ public enum VersionMatchMode { SAME_OR_OLDER } + record DialectVersionKey(Class dialect, DatabaseVersion version) { + public static DialectVersionKey of(SkipForDialect annotation) { + final Class dialectClass = annotation.dialectClass(); + int majorVersion = DatabaseVersion.NO_VERSION; + int minorVersion = DatabaseVersion.NO_VERSION; + int microVersion = DatabaseVersion.NO_VERSION; + if ( annotation.majorVersion() != -1 ) { + majorVersion = annotation.majorVersion(); + if ( annotation.minorVersion() != -1 ) { + minorVersion += annotation.minorVersion(); + if ( annotation.microVersion() != -1 ) { + microVersion += annotation.microVersion(); + } + } + } + return new DialectVersionKey( dialectClass, new SimpleDatabaseVersion( majorVersion, minorVersion, microVersion ) ); + } + } + private ConditionEvaluationResult evaluateSkipConditions(ExtensionContext context, Dialect dialect, String enabledResult) { final Collection effectiveSkips = TestingUtil.collectAnnotations( context, SkipForDialect.class, SkipForDialectGroup.class, (methodAnnotation, methodAnnotations, classAnnotation, classAnnotations) -> { - final LinkedHashMap, SkipForDialect> map = new LinkedHashMap<>(); + final LinkedHashMap map = new LinkedHashMap<>(); if ( classAnnotation != null ) { - map.put( classAnnotation.dialectClass(), classAnnotation ); + map.put( DialectVersionKey.of( classAnnotation ), classAnnotation ); } if ( classAnnotations != null ) { for ( SkipForDialect annotation : classAnnotations ) { - map.put( annotation.dialectClass(), annotation ); + map.put( DialectVersionKey.of( annotation ), annotation ); } } if ( methodAnnotation != null ) { - map.put( methodAnnotation.dialectClass(), methodAnnotation ); + map.put( DialectVersionKey.of( methodAnnotation ), methodAnnotation ); } if ( methodAnnotations != null ) { for ( SkipForDialect annotation : methodAnnotations ) { - map.put( annotation.dialectClass(), annotation ); + map.put( DialectVersionKey.of( annotation ), annotation ); } } return map.values(); diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/EntityManagerFactoryExtension.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/EntityManagerFactoryExtension.java index 7349b19c13bb..16c28c6ad797 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/EntityManagerFactoryExtension.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/EntityManagerFactoryExtension.java @@ -111,6 +111,9 @@ private static void collectProperties(PersistenceUnitInfoImpl pui, Jpa jpa) { private static PersistenceUnitInfoImpl createPersistenceUnitInfo(Jpa jpa) { final PersistenceUnitInfoImpl pui = new PersistenceUnitInfoImpl( jpa.persistenceUnitName() ); + // Use the context class loader for entity loading if configured, + // to make enhancement work for tests + pui.setClassLoader( Thread.currentThread().getContextClassLoader() ); pui.setTransactionType( jpa.transactionType() ); pui.setCacheMode( jpa.sharedCacheMode() ); pui.setValidationMode( jpa.validationMode() ); diff --git a/local-build-plugins/src/main/groovy/local.java-module.gradle b/local-build-plugins/src/main/groovy/local.java-module.gradle index 099502950d51..5c65e76f53cc 100644 --- a/local-build-plugins/src/main/groovy/local.java-module.gradle +++ b/local-build-plugins/src/main/groovy/local.java-module.gradle @@ -315,6 +315,11 @@ tasks.named("jar") { '-exportcontents': "*;version=${project.version}" ) } + metaInf { + from(rootProject.projectDir, { + include "LICENSE.txt" + }) + } } diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/DescriptorAccess.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/DescriptorAccess.java deleted file mode 100644 index 36c3623cfb79..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/DescriptorAccess.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.orm.docs; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; - -import jakarta.json.bind.Jsonb; -import jakarta.json.bind.JsonbBuilder; -import jakarta.json.bind.JsonbConfig; -import org.apache.http.HttpEntity; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; - -/** - * Helper for {@linkplain #loadProject() loading} the project documentation descriptor and - * {@linkplain #storeProject storing} it to file. - * - * @author Steve Ebersole - */ -public class DescriptorAccess { - public static final String DETAILS_URL = "https://docs.jboss.org/hibernate/_outdated-content/orm.json"; - - /** - * Load the descriptor - */ - public static ProjectDocumentationDescriptor loadProject() { - try ( final CloseableHttpClient httpClient = HttpClientBuilder.create().build() ) { - final HttpGet request = new HttpGet( DETAILS_URL ); - - try ( final CloseableHttpResponse response = httpClient.execute( request ) ) { - final HttpEntity responseEntity = response.getEntity(); - //noinspection resource - final Jsonb jsonb = JsonbBuilder.create( new JsonbConfig().withFormatting( true ) ); - return jsonb.fromJson( responseEntity.getContent(), ProjectDocumentationDescriptor.class ); - } - } - catch (IOException e) { - throw new RuntimeException( "Unable to create HttpClient", e ); - } - } - - /** - * Store the descriptor to file - */ - public static void storeProject(ProjectDocumentationDescriptor project, File jsonFile) { - prepareJsonFile( jsonFile ); - - //noinspection resource - final Jsonb jsonb = JsonbBuilder.create( new JsonbConfig().withFormatting( true ) ); - - try ( final FileWriter writer = new FileWriter( jsonFile ) ) { - jsonb.toJson( project, writer ); - } - catch (IOException e) { - throw new RuntimeException( "Unable to open write for JSON file : " + jsonFile.getPath(), e ); - } - } - - private static void prepareJsonFile(File jsonFile) { - if ( jsonFile.exists() ) { - final boolean deleted = jsonFile.delete(); - assert deleted; - } - - if ( ! jsonFile.getParentFile().exists() ) { - final boolean dirsMade = jsonFile.getParentFile().mkdirs(); - assert dirsMade; - } - - try { - final boolean created = jsonFile.createNewFile(); - assert created; - } - catch (IOException e) { - throw new RuntimeException( "Unable to create JSON file : `" + jsonFile.getPath() + "`", e ); - } - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/DocumentationPublishing.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/DocumentationPublishing.java deleted file mode 100644 index a3876318f067..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/DocumentationPublishing.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.orm.docs; - -import javax.inject.Inject; - -import org.gradle.api.Project; -import org.gradle.api.file.DirectoryProperty; -import org.gradle.api.file.RegularFile; -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.Property; -import org.gradle.api.provider.Provider; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -/** - * Gradle DSL extension for configuring documentation publishing - * - * @author Steve Ebersole - */ -public class DocumentationPublishing { - public static final String DSL_NAME = "documentationPublishing"; - - public static final String RSYNC_SERVER = "filemgmt-prod-sync.jboss.org"; - public static final String SFTP_SERVER = "filemgmt-prod.jboss.org"; - - public static final String DOC_SERVER_BASE_DIR = "/docs_htdocs/hibernate"; - - public static final String DESCRIPTOR_FILE = "doc-pub/orm.json"; - - private final Project project; - - private final DirectoryProperty stagingDirectory; - - private final Property rsyncDocServer; - private final Property sftpDocServer; - private final Property serverBaseDir; - - private final RegularFileProperty updatedJsonFile; - - private final ReleaseFamilyIdentifier releaseFamilyIdentifier; - - @Inject - public DocumentationPublishing(Project project) { - this.project = project; - - stagingDirectory = project.getObjects() - .directoryProperty() - .convention( project.getLayout().getBuildDirectory().dir( "documentation" ) ); - - - rsyncDocServer = project.getObjects() - .property( String.class ) - .convention( RSYNC_SERVER ); - - sftpDocServer = project.getObjects() - .property( String.class ) - .convention( SFTP_SERVER ); - - serverBaseDir = project.getObjects() - .property( String.class ) - .convention( DOC_SERVER_BASE_DIR ); - - updatedJsonFile = project.getObjects() - .fileProperty() - .convention( project.getLayout().getBuildDirectory().file( DESCRIPTOR_FILE ) ); - - releaseFamilyIdentifier = ReleaseFamilyIdentifier.parse( project.getVersion().toString() ); - } - - public ReleaseFamilyIdentifier getReleaseFamilyIdentifier() { - return releaseFamilyIdentifier; - } - - public Property getRsyncDocServer() { - return rsyncDocServer; - } - - public Property getSftpDocServer() { - return sftpDocServer; - } - - public Property getServerBaseDir() { - return serverBaseDir; - } - - public DirectoryProperty getStagingDirectory() { - return stagingDirectory; - } - - public Provider getUpdatedJsonFile() { - return updatedJsonFile; - } - - public void setUpdatedJsonFile(Object ref) { - updatedJsonFile.fileValue( project.file( ref ) ); - } - - public void updatedJsonFile(Object ref) { - updatedJsonFile.fileValue( project.file( ref ) ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/DocumentationPublishingPlugin.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/DocumentationPublishingPlugin.java deleted file mode 100644 index 4f8a79b3ba6c..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/DocumentationPublishingPlugin.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.orm.docs; - -import java.util.Locale; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.provider.Provider; -import org.gradle.api.tasks.TaskProvider; - -import static org.hibernate.orm.docs.DocumentationPublishing.DSL_NAME; -import static org.hibernate.orm.docs.GenerateDescriptorTask.GEN_DESC_TASK_NAME; -import static org.hibernate.orm.docs.PublishDescriptorTask.UPLOAD_DESC_TASK_NAME; -import static org.hibernate.orm.docs.PublishTask.UPLOAD_TASK_NAME; - -/** - * Plugin for helping with publishing documentation to the doc server -
    - *
  • Publishes a {@link DocumentationPublishing DSL extension} under {@value DocumentationPublishing#DSL_NAME}
  • - *
  • Creates a task to upload the documentation to the doc server - {@value PublishTask#UPLOAD_TASK_NAME}
  • - *
  • Creates a task to create the doc descriptor (JSON) file - {@value GenerateDescriptorTask#GEN_DESC_TASK_NAME}
  • - *
  • Creates a task to upload the doc descriptor (JSON) file to the doc server - {@value PublishDescriptorTask#UPLOAD_DESC_TASK_NAME}
  • - *
  • Creates a task to update symlinks on the doc server - {@value UpdateSymLinksTask#SYMLINKS_TASK_NAME}
  • - *
  • Creates a task to upload the migration guide to the doc server - {@value PublishMigrationGuide#NAME}
  • - *
- * - * @author Steve Ebersole - */ -public class DocumentationPublishingPlugin implements Plugin { - @Override - public void apply(Project project) { - final DocumentationPublishing docPubDsl = project.getExtensions().create( DSL_NAME, DocumentationPublishing.class ); - - final boolean isSnapshot = project.getVersion().toString().endsWith( "-SNAPSHOT" ); - final boolean isFinal = project.getVersion().toString().endsWith( ".Final" ); - - final TaskProvider generateDescriptorTask = project.getTasks().register( - GEN_DESC_TASK_NAME, - GenerateDescriptorTask.class, - (task) -> { - task.getCurrentlyBuildingFamily().convention( docPubDsl.getReleaseFamilyIdentifier() ); - task.getJsonFile().convention( docPubDsl.getUpdatedJsonFile() ); - - task.onlyIf( (t) -> isFinal ); - } - ); - - final TaskProvider uploadDescriptorTask = project.getTasks().register( - UPLOAD_DESC_TASK_NAME, - PublishDescriptorTask.class, - (task) -> { - task.getDocDescriptorUploadUrl().convention( defaultDescriptorUploadUrl( docPubDsl ) ); - task.getJsonFile().convention( docPubDsl.getUpdatedJsonFile() ); - - task.dependsOn( generateDescriptorTask ); - task.onlyIf( (t) -> generateDescriptorTask.get().getDidWork() && generateDescriptorTask.get().needsUpload() ); - } - ); - - //noinspection unused - final TaskProvider uploadTask = project.getTasks().register( - UPLOAD_TASK_NAME, - PublishTask.class, - (task) -> { - task.getBuildingFamily().convention( docPubDsl.getReleaseFamilyIdentifier() ); - task.getStagingDirectory().convention( docPubDsl.getStagingDirectory() ); - task.getDocServerUrl().convention( defaultDocUploadUrl( docPubDsl ) ); - - task.dependsOn( uploadDescriptorTask ); - task.onlyIf( (t) -> !isSnapshot ); - } - ); - - //noinspection unused - final TaskProvider symLinkTask = project.getTasks().register( - UpdateSymLinksTask.SYMLINKS_TASK_NAME, - UpdateSymLinksTask.class, - (task) -> { - task.getBuildingFamily().convention( docPubDsl.getReleaseFamilyIdentifier() ); - task.getSftpDocServer().convention( docPubDsl.getSftpDocServer() ); - task.getServerBaseDir().convention( docPubDsl.getServerBaseDir() ); - - task.dependsOn( generateDescriptorTask ); - task.dependsOn( uploadTask ); - task.onlyIf( (t) -> generateDescriptorTask.get().getDidWork() && generateDescriptorTask.get().needsSymLinkUpdate() ); - } - ); - - //noinspection unused - final TaskProvider publishMigrationGuideTask = project.getTasks().register( - PublishMigrationGuide.NAME, - PublishMigrationGuide.class, - (task) -> { - task.getCurrentlyBuildingFamily().convention( docPubDsl.getReleaseFamilyIdentifier() ); - task.getDocServerUrl().convention( defaultDocUploadUrl( docPubDsl ) ); - task.getMigrationGuideDirectory().convention( project.getLayout().getBuildDirectory().dir( "documentation/migration-guide" ) ); - } - ); - } - - private Provider defaultDescriptorUploadUrl(DocumentationPublishing dsl) { - return dsl.getRsyncDocServer() - .map( (server) -> String.format( Locale.ROOT, "%s:%s/_outdated-content/orm.json", server, dsl.getServerBaseDir().get() ) ); - } - - private Provider defaultDocUploadUrl(DocumentationPublishing dsl) { - return dsl.getRsyncDocServer() - .map( (server) -> String.format( Locale.ROOT, "%s:%s/orm", server, dsl.getServerBaseDir().get() ) ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/GenerateDescriptorTask.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/GenerateDescriptorTask.java deleted file mode 100644 index 33691614b62e..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/GenerateDescriptorTask.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.orm.docs; - -import java.io.File; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; - -import org.gradle.api.DefaultTask; -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.Property; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.OutputFile; -import org.gradle.api.tasks.TaskAction; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -/** - * Task for creating the JSON "documentation descriptor" for ORM - * - * @author Steve Ebersole - */ -public abstract class GenerateDescriptorTask extends DefaultTask { - public static final String GEN_DESC_TASK_NAME = "generateDocumentationDescriptor"; - - private final Property currentlyBuildingFamily; - private final RegularFileProperty jsonFile; - - private boolean needsUpload; - private boolean needsSymLinkUpdate; - - public GenerateDescriptorTask() { - setGroup( "documentation" ); - setDescription( "Generates the documentation publication descriptor (JSON)" ); - - currentlyBuildingFamily = getProject().getObjects().property( ReleaseFamilyIdentifier.class ); - jsonFile = getProject().getObjects().fileProperty(); - } - - @Input - public Property getCurrentlyBuildingFamily() { - return currentlyBuildingFamily; - } - - @OutputFile - public RegularFileProperty getJsonFile() { - return jsonFile; - } - - /** - * Whether we determined, during {@linkplain #generateDescriptor}, that uploading the - * doc descriptor was needed. - * - * @see PublishDescriptorTask - */ - public boolean needsUpload() { - return getDidWork() && needsUpload; - } - - /** - * Whether we determined, during {@linkplain #generateDescriptor}, that updating the - * doc server symlinks was needed. - * - * @see UpdateSymLinksTask - */ - public boolean needsSymLinkUpdate() { - return getDidWork() && needsSymLinkUpdate; - } - - @TaskAction - public void generateDescriptor() { - final ProjectDocumentationDescriptor descriptor = DescriptorAccess.loadProject(); - final boolean isFinal = getProject().getVersion().toString().endsWith( ".Final" ); - - final Set processedReleases = new HashSet<>(); - ReleaseFamilyIdentifier newest = null; - boolean foundCurrentRelease = false; - - final Iterator itr = descriptor.getReleaseFamilies().iterator(); - while ( itr.hasNext() ) { - final ReleaseFamilyDocumentation releaseFamily = itr.next(); - - // NOTE: sometimes releases get duplicated in the descriptor... - // let's clean those up if we run across them - if ( !processedReleases.add( releaseFamily.getName() ) ) { - itr.remove(); - needsUpload = true; - continue; - } - - if ( newest == null - || releaseFamily.getName().newerThan( newest ) ) { - newest = releaseFamily.getName(); - } - - if ( releaseFamily.getName().equals( currentlyBuildingFamily.get() ) ) { - foundCurrentRelease = true; - } - } - - if ( isFinal ) { - // we are releasing a Final - possibly do some other things - - if ( !foundCurrentRelease ) { - // this release is not yet tracked in the descriptor - add it - final ReleaseFamilyDocumentation newEntry = new ReleaseFamilyDocumentation(); - newEntry.setName( currentlyBuildingFamily.get() ); - descriptor.addReleaseFamily( newEntry ); - setDidWork( true ); - needsUpload = true; - } - - if ( currentlyBuildingFamily.get().newerThan( newest ) ) { - // this release is newer than any currently tracked in the descriptor - descriptor.setStableFamily( currentlyBuildingFamily.get() ); - setDidWork( true ); - needsSymLinkUpdate = true; - } - } - - DescriptorAccess.storeProject( descriptor, jsonFile.get().getAsFile() ); - } - - public static void main(String... args) { - final File jsonFile = new File( "/tmp/hibernate-orm-build/doc-pub/orm.json" ); - final ProjectDocumentationDescriptor projectDoc = DescriptorAccess.loadProject(); - DescriptorAccess.storeProject( projectDoc, jsonFile ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/ProjectDocumentationDescriptor.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/ProjectDocumentationDescriptor.java deleted file mode 100644 index daff4a0ac91d..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/ProjectDocumentationDescriptor.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.orm.docs; - -import java.util.List; -import java.util.Map; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -import jakarta.json.bind.annotation.JsonbProperty; -import jakarta.json.bind.annotation.JsonbPropertyOrder; -import jakarta.json.bind.annotation.JsonbTypeAdapter; - -/** - * Binding for the doc-pub descriptor (JSON) file - * - * @author Steve Ebersole - */ -@JsonbPropertyOrder( {"name", "stableFamily", "singlePageDetails", "multiPageDetails", "releaseFamilies" } ) -public class ProjectDocumentationDescriptor { - @JsonbProperty( "project" ) - private String name; - - @JsonbProperty( "stable" ) - @JsonbTypeAdapter( ReleaseFamilyIdentifierMarshalling.class ) - private ReleaseFamilyIdentifier stableFamily; - - @JsonbProperty( "versions" ) - private List releaseFamilies; - - @JsonbProperty( "multi" ) - private Map multiPageDetails; - @JsonbProperty( "single" ) - private Map singlePageDetails; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public ReleaseFamilyIdentifier getStableFamily() { - return stableFamily; - } - - public void setStableFamily(ReleaseFamilyIdentifier stableFamily) { - this.stableFamily = stableFamily; - } - - public List getReleaseFamilies() { - return releaseFamilies; - } - - public void setReleaseFamilies(List releaseFamilies) { - this.releaseFamilies = releaseFamilies; - } - - public void addReleaseFamily(ReleaseFamilyDocumentation familyDetails) { - // Add new entries at the top - releaseFamilies.add( 0, familyDetails ); - } - - public Map getMultiPageDetails() { - return multiPageDetails; - } - - public void setMultiPageDetails(Map multiPageDetails) { - this.multiPageDetails = multiPageDetails; - } - - public Map getSinglePageDetails() { - return singlePageDetails; - } - - public void setSinglePageDetails(Map singlePageDetails) { - this.singlePageDetails = singlePageDetails; - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishDescriptorTask.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishDescriptorTask.java deleted file mode 100644 index ed3df756cea0..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishDescriptorTask.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.orm.docs; - -import org.gradle.api.DefaultTask; -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.Property; -import org.gradle.api.provider.Provider; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputFile; -import org.gradle.api.tasks.SkipWhenEmpty; -import org.gradle.api.tasks.TaskAction; - -/** - * @author Steve Ebersole - */ -public abstract class PublishDescriptorTask extends DefaultTask { - public static final String UPLOAD_DESC_TASK_NAME = "uploadDocumentationDescriptor"; - - private final Provider projectVersion; - private final Property docDescriptorUploadUrl; - private final RegularFileProperty jsonFile; - - public PublishDescriptorTask() { - setGroup( "documentation" ); - setDescription( "Publishes the documentation publication descriptor (JSON)" ); - - projectVersion = getProject().provider( () -> getProject().getVersion() ); - docDescriptorUploadUrl = getProject().getObjects().property( String.class ); - jsonFile = getProject().getObjects().fileProperty(); - } - - @InputFile - @SkipWhenEmpty - public RegularFileProperty getJsonFile() { - return jsonFile; - } - - @Input - public Property getDocDescriptorUploadUrl() { - return docDescriptorUploadUrl; - } - - @Input - public Provider getProjectVersion() { - return projectVersion; - } - - - @TaskAction - public void uploadDescriptor() { - final String url = docDescriptorUploadUrl.get(); - RsyncHelper.rsync( jsonFile.get(), url, getProject() ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishMigrationGuide.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishMigrationGuide.java deleted file mode 100644 index f433104997da..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishMigrationGuide.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.orm.docs; - -import org.gradle.api.DefaultTask; -import org.gradle.api.file.DirectoryProperty; -import org.gradle.api.provider.Property; -import org.gradle.api.provider.Provider; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputDirectory; -import org.gradle.api.tasks.PathSensitive; -import org.gradle.api.tasks.PathSensitivity; -import org.gradle.api.tasks.TaskAction; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -/** - * @author Steve Ebersole - */ -public abstract class PublishMigrationGuide extends DefaultTask { - public static final String NAME = "publishMigrationGuide"; - - private final Provider projectVersion; - private final Property currentlyBuildingFamily; - private final Property docServerUrl; - private final DirectoryProperty migrationGuideDirectory; - - public PublishMigrationGuide() { - setGroup( "documentation" ); - setDescription( "Publishes the migration-guide associated with the current branch. " + - "Intended for incremental publishing of the guide for corrections, etc. without doing a full release. " + - "Note that this is not needed when doing a release as the migration-guide is published as part of that workflow." ); - - getInputs().property( "hibernate-version", getProject().getVersion() ); - - projectVersion = getProject().provider( () -> getProject().getVersion() ); - currentlyBuildingFamily = getProject().getObjects().property( ReleaseFamilyIdentifier.class ); - - docServerUrl = getProject().getObjects().property( String.class ); - migrationGuideDirectory = getProject().getObjects().directoryProperty(); - } - - @Input - public Provider getProjectVersion() { - return projectVersion; - } - - @InputDirectory - @PathSensitive(PathSensitivity.RELATIVE) - public DirectoryProperty getMigrationGuideDirectory() { - return migrationGuideDirectory; - } - - @Input - public Property getDocServerUrl() { - return docServerUrl; - } - - - @Input - public Property getCurrentlyBuildingFamily() { - return currentlyBuildingFamily; - } - - @TaskAction - public void uploadMigrationGuide() { - final String base = docServerUrl.get(); - final String normalizedBase = base.endsWith( "/" ) ? base : base + "/"; - final String url = normalizedBase + currentlyBuildingFamily.get().toExternalForm() + "/migration-guide/"; - - RsyncHelper.rsync( migrationGuideDirectory.get(), url, getProject() ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishTask.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishTask.java deleted file mode 100644 index 72ca4da38ebb..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishTask.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.orm.docs; - -import org.gradle.api.DefaultTask; -import org.gradle.api.file.Directory; -import org.gradle.api.file.DirectoryProperty; -import org.gradle.api.provider.Property; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputDirectory; -import org.gradle.api.tasks.TaskAction; -import org.gradle.process.ExecResult; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -/** - * @author Steve Ebersole - */ -public abstract class PublishTask extends DefaultTask { - public static final String UPLOAD_TASK_NAME = "uploadDocumentation"; - - private final Property buildingFamily; - private final Property docServerUrl; - private final DirectoryProperty stagingDirectory; - - public PublishTask() { - setGroup( "documentation" ); - setDescription( "Publish documentation to the doc server" ); - - buildingFamily = getProject().getObjects().property( ReleaseFamilyIdentifier.class ); - docServerUrl = getProject().getObjects().property( String.class ); - stagingDirectory = getProject().getObjects().directoryProperty(); - } - - @Input - public Property getDocServerUrl() { - return docServerUrl; - } - - @Input - public Property getBuildingFamily() { - return buildingFamily; - } - - @InputDirectory - public Property getStagingDirectory() { - return stagingDirectory; - } - - @TaskAction - public void uploadDocumentation() { - final String releaseFamily = buildingFamily.get().toExternalForm(); - final String base = docServerUrl.get(); - final String normalizedBase = base.endsWith( "/" ) ? base : base + "/"; - final String url = normalizedBase + releaseFamily; - - final String stagingDirPath = stagingDirectory.get().getAsFile().getAbsolutePath(); - final String stagingDirPathContent = stagingDirPath.endsWith( "/" ) ? stagingDirPath : stagingDirPath + "/"; - - getProject().getLogger().lifecycle( "Uploading documentation `{}` -> `{}`", stagingDirPath, url ); - final ExecResult result = getProject().exec( (exec) -> { - exec.executable( "rsync" ); - exec.args("-avz", "--delete", stagingDirPathContent, url ); - } ); - getProject().getLogger().lifecycle( "Done uploading documentation - {}", result.getExitValue() == 0 ? "success" : "failure" ); - setDidWork( true ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/ReleaseFamilyDocumentation.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/ReleaseFamilyDocumentation.java deleted file mode 100644 index eddef320d253..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/ReleaseFamilyDocumentation.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.orm.docs; - -import java.util.HashMap; -import java.util.Map; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -import jakarta.json.bind.annotation.JsonbProperty; -import jakarta.json.bind.annotation.JsonbPropertyOrder; -import jakarta.json.bind.annotation.JsonbTypeAdapter; - -/** - * Binding for the doc-pub descriptor (JSON) related to a specific release family. - * - * @see ProjectDocumentationDescriptor - * - * @author Steve Ebersole - */ -@JsonbPropertyOrder( { "name", "redirects" } ) -public class ReleaseFamilyDocumentation { - @JsonbProperty( "version" ) - @JsonbTypeAdapter( ReleaseFamilyIdentifierMarshalling.class ) - private ReleaseFamilyIdentifier name; - private Map redirects; - - public ReleaseFamilyDocumentation() { - } - - /** - * The release family, e.g. `6.0` or `5.6` - */ - public ReleaseFamilyIdentifier getName() { - return name; - } - - public void setName(ReleaseFamilyIdentifier name) { - this.name = name; - } - - public Map getRedirects() { - return redirects; - } - - public void setRedirects(Map redirects) { - this.redirects = redirects; - } - - public void redirect(String from, String to) { - if ( redirects == null ) { - redirects = new HashMap<>(); - } - redirects.put( from, to ); - } - - @Override - public String toString() { - return "ReleaseFamilyDocumentation( " + name + ")"; - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/ReleaseFamilyIdentifierMarshalling.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/ReleaseFamilyIdentifierMarshalling.java deleted file mode 100644 index 1676359520a8..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/ReleaseFamilyIdentifierMarshalling.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.orm.docs; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -import jakarta.json.Json; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; -import jakarta.json.bind.adapter.JsonbAdapter; - -/** - * @author Steve Ebersole - */ -public class ReleaseFamilyIdentifierMarshalling implements JsonbAdapter { - @Override - public JsonValue adaptToJson(ReleaseFamilyIdentifier obj) throws Exception { - return Json.createValue( obj.toExternalForm() ); - } - - @Override - public ReleaseFamilyIdentifier adaptFromJson(JsonValue obj) throws Exception { - assert obj.getValueType() == JsonValue.ValueType.STRING; - final JsonString jsonString = (JsonString) obj; - return ReleaseFamilyIdentifier.parse( jsonString.getString() ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/RsyncHelper.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/RsyncHelper.java deleted file mode 100644 index a7d93c1d1131..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/RsyncHelper.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.orm.docs; - -import org.gradle.api.Project; -import org.gradle.api.file.FileSystemLocation; - -/** - * Helper for performing rsync system commands, mainly used to centralize - * the command options - * - * @author Steve Ebersole - */ -public class RsyncHelper { - public static void rsync( - FileSystemLocation source, - String targetUrl, - Project project) { - project.exec( (exec) -> { - exec.executable( "rsync" ); - exec.args( "--port=2222", "-avz", source.getAsFile().getAbsolutePath(), targetUrl ); - } ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/UpdateSymLinksTask.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/UpdateSymLinksTask.java deleted file mode 100644 index 19e43a9c31a0..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/UpdateSymLinksTask.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.orm.docs; - -import java.io.BufferedReader; -import java.io.Closeable; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Locale; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; - -import org.gradle.api.DefaultTask; -import org.gradle.api.provider.Property; -import org.gradle.api.tasks.TaskAction; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -import static org.hibernate.orm.docs.DocumentationPublishing.DOC_SERVER_BASE_DIR; -import static org.hibernate.orm.docs.DocumentationPublishing.SFTP_SERVER; - -/** - * Updates the "current" and "stable" symlinks on the doc server - * - * @author Steve Ebersole - */ -public class UpdateSymLinksTask extends DefaultTask { - public static final String SYMLINKS_TASK_NAME = "updateDocSymLinks"; - - private final Property sftpDocServer; - private final Property serverBaseDir; - private final Property buildingFamily; - - public UpdateSymLinksTask() { - setGroup( "documentation" ); - setDescription( "Updates the 'current' and 'stable' symlinks on the documentation server" ); - - buildingFamily = getProject().getObjects().property( ReleaseFamilyIdentifier.class ); - sftpDocServer = getProject().getObjects().property( String.class ); - serverBaseDir = getProject().getObjects().property( String.class ); - } - - public Property getBuildingFamily() { - return buildingFamily; - } - - public Property getSftpDocServer() { - return sftpDocServer; - } - - public Property getServerBaseDir() { - return serverBaseDir; - } - - @TaskAction - public void updateSymLinks() throws Exception { - updateSymLinks( buildingFamily.get().toExternalForm(), sftpDocServer.get(), serverBaseDir.get() ); - } - - private static void updateSymLinks(String releaseName, String sftpServer, String serverBaseDir) throws Exception { - final File commandFile = createCommandFile( releaseName, serverBaseDir ); - System.out.println( "SFTP command file : " + commandFile.getAbsolutePath() ); - - final Process sftpProcess = new ProcessBuilder() - .command( "sh", "sftp", "-b", commandFile.getAbsolutePath(), sftpServer ) - .redirectInput( ProcessBuilder.Redirect.INHERIT ) - .start(); - - ExecutorService service = Executors.newFixedThreadPool( 2 ); - try ( InputStream is = sftpProcess.getInputStream(); InputStream es = sftpProcess.getErrorStream(); - Closeable pool = service::shutdownNow ) { - service.submit( () -> drain( is, System.out::println ) ); - service.submit( () -> drain( es, System.err::println ) ); - service.shutdown(); - - final boolean isFinished = sftpProcess.waitFor( 15, TimeUnit.SECONDS ); - if ( !isFinished ) { - System.out.println( "Forcibly ending sftp" ); - sftpProcess.destroyForcibly(); - } - } - } - - private static void drain(InputStream stream, Consumer consumer) { - try (InputStreamReader in = new InputStreamReader( stream, StandardCharsets.UTF_8 ); - BufferedReader bufferedReader = new BufferedReader( in ); ) { - bufferedReader.lines().forEach( consumer ); - } - catch (IOException e) { - throw new RuntimeException( e ); - } - } - - - private static File createCommandFile(String releaseName, String serverBaseDir) throws IOException { - final File commandFile = File.createTempFile( "hibernate-orm-release-doc-symlink-" + releaseName, "-cmd.txt" ); - - try (FileWriter commandFileWriter = new FileWriter( commandFile )) { - commandFileWriter.write( "cd " + serverBaseDir + "/stable\n" ); - commandFileWriter.write( "-rm orm\n" ); - commandFileWriter.write( String.format( Locale.ROOT, "ln -s ../orm/%s orm\n", releaseName ) ); - - commandFileWriter.write( "cd " + serverBaseDir + "/orm\n" ); - commandFileWriter.write( "-rm current\n" ); - commandFileWriter.write( String.format( Locale.ROOT, "ln -s %s current\n", releaseName ) ); - - commandFileWriter.flush(); - } - - return commandFile; - } - - public static void main(String[] args) throws Exception { - System.out.println( "Starting UpdateSymLinksTask" ); - updateSymLinks( "6.6", SFTP_SERVER, DOC_SERVER_BASE_DIR ); - } -} diff --git a/nightly.Jenkinsfile b/nightly.Jenkinsfile deleted file mode 100644 index f64f343e33e9..000000000000 --- a/nightly.Jenkinsfile +++ /dev/null @@ -1,404 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ - -import groovy.transform.Field -import io.jenkins.blueocean.rest.impl.pipeline.PipelineNodeGraphVisitor -import io.jenkins.blueocean.rest.impl.pipeline.FlowNodeWrapper -import org.jenkinsci.plugins.workflow.support.steps.build.RunWrapper - -/* - * See https://github.com/hibernate/hibernate-jenkins-pipeline-helpers - */ -@Library('hibernate-jenkins-pipeline-helpers') _ -import org.hibernate.jenkins.pipeline.helpers.job.JobHelper - -@Field final String DEFAULT_JDK_VERSION = '21' -@Field final String DEFAULT_JDK_TOOL = "OpenJDK ${DEFAULT_JDK_VERSION} Latest" -@Field final String NODE_PATTERN_BASE = 'Worker&&Containers' -@Field List environments - -this.helper = new JobHelper(this) - -helper.runWithNotification { -stage('Configure') { - this.environments = [ - // Minimum supported versions - new BuildEnvironment( dbName: 'hsqldb_2_6' ), - new BuildEnvironment( dbName: 'mysql_8_0' ), - new BuildEnvironment( dbName: 'mariadb_10_6' ), - new BuildEnvironment( dbName: 'postgresql_13' ), - new BuildEnvironment( dbName: 'edb_13' ), - new BuildEnvironment( dbName: 'db2_11_5' ), // Unfortunately there is no SQL Server 11.1 image, but 11.5 should mostly have feature parity - new BuildEnvironment( dbName: 'mssql_2017' ), // Unfortunately there is no SQL Server 2008 image, so we have to test with 2017 -// new BuildEnvironment( dbName: 'sybase_16' ), // There only is a Sybase ASE 16 image, so no pint in testing that nightly - new BuildEnvironment( dbName: 'sybase_jconn' ), - // Long running databases - new BuildEnvironment( dbName: 'cockroachdb', node: 'cockroachdb', longRunning: true ), - new BuildEnvironment( dbName: 'hana_cloud', dbLockableResource: 'hana-cloud', dbLockResourceAsHost: true ) - ]; - - helper.configure { - file 'job-configuration.yaml' - // We don't require the following, but the build helper plugin apparently does - jdk { - defaultTool DEFAULT_JDK_TOOL - } - maven { - defaultTool 'Apache Maven 3.8' - } - } - properties([ - buildDiscarder( - logRotator(daysToKeepStr: '30', numToKeepStr: '10') - ), - rateLimitBuilds(throttle: [count: 1, durationName: 'day', userBoost: true]), - // If two builds are about the same branch or pull request, - // the older one will be aborted when the newer one starts. - disableConcurrentBuilds(abortPrevious: true), - helper.generateNotificationProperty() - ]) -} - -// Avoid running the pipeline on branch indexing -if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { - print "INFO: Build skipped due to trigger being Branch Indexing" - currentBuild.result = 'NOT_BUILT' - return -} - -stage('Build') { - Map executions = [:] - Map> state = [:] - environments.each { BuildEnvironment buildEnv -> - // Don't build environments for newer JDKs when this is a PR - if ( helper.scmSource.pullRequest && buildEnv.testJdkVersion ) { - return - } - state[buildEnv.tag] = [:] - executions.put(buildEnv.tag, { - runBuildOnNode(buildEnv.node ?: NODE_PATTERN_BASE) { - def testJavaHome - if ( buildEnv.testJdkVersion ) { - testJavaHome = tool(name: "OpenJDK ${buildEnv.testJdkVersion} Latest", type: 'jdk') - } - def javaHome = tool(name: DEFAULT_JDK_TOOL, type: 'jdk') - // Use withEnv instead of setting env directly, as that is global! - // See https://github.com/jenkinsci/pipeline-plugin/blob/master/TUTORIAL.md - withEnv(["JAVA_HOME=${javaHome}", "PATH+JAVA=${javaHome}/bin"]) { - state[buildEnv.tag]['additionalOptions'] = '' - if ( testJavaHome ) { - state[buildEnv.tag]['additionalOptions'] = state[buildEnv.tag]['additionalOptions'] + - " -Ptest.jdk.version=${buildEnv.testJdkVersion} -Porg.gradle.java.installations.paths=${javaHome},${testJavaHome}" - } - if ( buildEnv.testJdkLauncherArgs ) { - state[buildEnv.tag]['additionalOptions'] = state[buildEnv.tag]['additionalOptions'] + - " -Ptest.jdk.launcher.args=${buildEnv.testJdkLauncherArgs}" - } - state[buildEnv.tag]['containerName'] = null; - stage('Checkout') { - checkout scm - } - tryFinally({ - stage('Start database') { - switch (buildEnv.dbName) { - case "hsqldb_2_6": - state[buildEnv.tag]['additionalOptions'] = state[buildEnv.tag]['additionalOptions'] + - " -Pgradle.libs.versions.hsqldb=2.6.1" - break; - case "mysql_8_0": - sh "./docker_db.sh mysql_8_0" - state[buildEnv.tag]['containerName'] = "mysql" - break; - case "mariadb_10_6": - sh "./docker_db.sh mariadb_10_6" - state[buildEnv.tag]['containerName'] = "mariadb" - break; - case "postgresql_13": - sh "./docker_db.sh postgresql_13" - state[buildEnv.tag]['containerName'] = "postgres" - break; - case "edb_13": - sh "./docker_db.sh edb_13" - state[buildEnv.tag]['containerName'] = "edb" - break; - case "db2_11_5": - sh "./docker_db.sh db2_11_5" - state[buildEnv.tag]['containerName'] = "db2" - break; - case "mssql_2017": - sh "./docker_db.sh mssql_2017" - state[buildEnv.tag]['containerName'] = "mssql" - break; - case "sybase_jconn": - sh "./docker_db.sh sybase" - state[buildEnv.tag]['containerName'] = "sybase" - break; - case "cockroachdb": - sh "./docker_db.sh cockroachdb" - state[buildEnv.tag]['containerName'] = "cockroach" - break; - } - } - stage('Test') { - String args = "${buildEnv.additionalOptions ?: ''} ${state[buildEnv.tag]['additionalOptions'] ?: ''}" - withEnv(["RDBMS=${buildEnv.dbName}"]) { - tryFinally({ - if (buildEnv.dbLockableResource == null) { - withCredentials([file(credentialsId: 'sybase-jconnect-driver', variable: 'jconnect_driver')]) { - sh 'cp -f $jconnect_driver ./drivers/jconn4.jar' - timeout( [time: buildEnv.longRunning ? 480 : 120, unit: 'MINUTES'] ) { - ciBuild buildEnv, args - } - } - } - else { - lock(label: buildEnv.dbLockableResource, quantity: 1, variable: 'LOCKED_RESOURCE') { - if ( buildEnv.dbLockResourceAsHost ) { - args += " -DdbHost=${LOCKED_RESOURCE}" - } - timeout( [time: buildEnv.longRunning ? 480 : 120, unit: 'MINUTES'] ) { - ciBuild buildEnv, args - } - } - } - }, { // Finally - junit '**/target/test-results/test/*.xml,**/target/test-results/testKitTest/*.xml' - }) - } - } - }, { // Finally - if ( state[buildEnv.tag]['containerName'] != null ) { - sh "docker rm -f ${state[buildEnv.tag]['containerName']}" - } - // Skip this for PRs - if ( !env.CHANGE_ID && buildEnv.notificationRecipients != null ) { - handleNotifications(currentBuild, buildEnv) - } - }) - } - } - }) - } - // Don't run additional checks when this is a PR - if ( !helper.scmSource.pullRequest ) { - executions.put('Reproducible build check', { - runBuildOnNode(NODE_PATTERN_BASE) { - def javaHome = tool(name: DEFAULT_JDK_TOOL, type: 'jdk') - // Use withEnv instead of setting env directly, as that is global! - // See https://github.com/jenkinsci/pipeline-plugin/blob/master/TUTORIAL.md - withEnv(["JAVA_HOME=${javaHome}", "PATH+JAVA=${javaHome}/bin"]) { - stage('Checkout') { - checkout scm - } - stage('Test') { - withGradle { - def tempDir = pwd(tmp: true) - def repo1 = tempDir + '/repo1' - def repo2 = tempDir + '/repo2' - // build Hibernate ORM two times without any cache and "publish" the resulting artifacts to different maven repositories - // so that we can compare them afterwards: - sh "./gradlew --no-daemon clean publishToMavenLocal --no-build-cache -Dmaven.repo.local=${repo1}" - sh "./gradlew --no-daemon clean publishToMavenLocal --no-build-cache -Dmaven.repo.local=${repo2}" - - sh "sh ci/compare-build-results.sh ${repo1} ${repo2}" - sh "cat .buildcompare" - } - } - } - } - }) - executions.put('Strict JAXP configuration', { - runBuildOnNode(NODE_PATTERN_BASE) { - // we want to test with JDK 23 where the strict settings were introduced - def testJavaHome = tool(name: "OpenJDK 23 Latest", type: 'jdk') - def javaHome = tool(name: DEFAULT_JDK_TOOL, type: 'jdk') - // Use withEnv instead of setting env directly, as that is global! - // See https://github.com/jenkinsci/pipeline-plugin/blob/master/TUTORIAL.md - withEnv(["JAVA_HOME=${javaHome}", "PATH+JAVA=${javaHome}/bin"]) { - stage('Checkout') { - checkout scm - } - stage('Test') { - withGradle { - def tempDir = pwd(tmp: true) - def jaxpStrictProperties = tempDir + '/jaxp-strict.properties' - def jaxpStrictTemplate = testJavaHome + '/conf/jaxp-strict.properties.template' - - echo 'Copy strict JAXP configuration properties.' - sh "cp $jaxpStrictTemplate $jaxpStrictProperties" - - // explicitly calling toString here to prevent Jenkins failures like: - // > Scripts not permitted to use method groovy.lang.GroovyObject invokeMethod java.lang.String java.lang.Object (org.codehaus.groovy.runtime.GStringImpl positive) - String args = ("-Ptest.jdk.version=23 -Porg.gradle.java.installations.paths=${javaHome},${testJavaHome}" - + " -Ptest.jdk.launcher.args=\"-Djava.xml.config.file=${jaxpStrictProperties}\"").toString() - - timeout( [time: 60, unit: 'MINUTES'] ) { - ciBuild(args) - } - } - } - } - } - }) - } - parallel(executions) -} - -} // End of helper.runWithNotification - -// Job-specific helpers - -class BuildEnvironment { - String testJdkVersion - String testJdkLauncherArgs - String dbName = 'h2' - String node - String dbLockableResource - boolean dbLockResourceAsHost - String additionalOptions - String notificationRecipients - boolean longRunning - - String toString() { getTag() } - String getTag() { "${node ? node + "_" : ''}${testJdkVersion ? 'jdk_' + testJdkVersion + '_' : '' }${dbName}" } -} - -void runBuildOnNode(String label, Closure body) { - node( label ) { - pruneDockerContainers() - tryFinally(body, { - // If this is a PR, we clean the workspace at the end - if ( env.CHANGE_BRANCH != null ) { - cleanWs() - } - pruneDockerContainers() - }) - } -} - -void ciBuild(buildEnv, String args) { - // On untrusted nodes, we use the same access key as for PRs: - // it has limited access, essentially it can only push build scans. - def develocityCredentialsId = buildEnv.node ? 'develocity.commonhaus.dev-access-key-pr' : 'develocity.commonhaus.dev-access-key' - - ciBuild(develocityCredentialsId, args) -} - -void ciBuild(String args) { - ciBuild('develocity.commonhaus.dev-access-key-pr', args) -} - -void ciBuild(String develocityCredentialsId, String args) { - withCredentials([string(credentialsId: develocityCredentialsId, - variable: 'DEVELOCITY_ACCESS_KEY')]) { - withGradle { // withDevelocity, actually: https://plugins.jenkins.io/gradle/#plugin-content-capturing-build-scans-from-jenkins-pipeline - sh "./ci/build.sh $args" - } - } -} - -void pruneDockerContainers() { - if ( !sh( script: 'command -v docker || true', returnStdout: true ).trim().isEmpty() ) { - sh 'docker container prune -f || true' - sh 'docker image prune -f || true' - sh 'docker network prune -f || true' - sh 'docker volume prune -f || true' - } -} - -void handleNotifications(currentBuild, buildEnv) { - def currentResult = getParallelResult(currentBuild, buildEnv.tag) - boolean success = currentResult == 'SUCCESS' || currentResult == 'UNKNOWN' - def previousResult = currentBuild.previousBuild == null ? null : getParallelResult(currentBuild.previousBuild, buildEnv.tag) - - // Ignore success after success - if ( !( success && previousResult == 'SUCCESS' ) ) { - def subject - def body - if ( success ) { - if ( previousResult != 'SUCCESS' && previousResult != null ) { - subject = "${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Fixed" - body = """

${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Fixed:

-

Check console output at ${env.BUILD_URL} to view the results.

""" - } - else { - subject = "${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Success" - body = """

${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Success:

-

Check console output at ${env.BUILD_URL} to view the results.

""" - } - } - else if (currentBuild.rawBuild.getActions(jenkins.model.InterruptedBuildAction.class).isEmpty()) { - // If there are interrupted build actions, this means the build was cancelled, probably superseded - // Thanks to https://issues.jenkins.io/browse/JENKINS-43339 for the "hack" to determine this - if ( currentResult == 'FAILURE' ) { - if ( previousResult != null && previousResult == "FAILURE" ) { - subject = "${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Still failing" - body = """

${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Still failing:

-

Check console output at ${env.BUILD_URL} to view the results.

""" - } - else { - subject = "${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Failure" - body = """

${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Failure:

-

Check console output at ${env.BUILD_URL} to view the results.

""" - } - } - else { - subject = "${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - ${currentResult}" - body = """

${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - ${currentResult}:

-

Check console output at ${env.BUILD_URL} to view the results.

""" - } - } - - emailext( - subject: subject, - body: body, - to: buildEnv.notificationRecipients - ) - } -} - -@NonCPS -String getParallelResult( RunWrapper build, String parallelBranchName ) { - def visitor = new PipelineNodeGraphVisitor( build.rawBuild ) - def branch = visitor.pipelineNodes.find{ it.type == FlowNodeWrapper.NodeType.PARALLEL && parallelBranchName == it.displayName } - if ( branch == null ) { - echo "Couldn't find parallel branch name '$parallelBranchName'. Available parallel branch names:" - visitor.pipelineNodes.findAll{ it.type == FlowNodeWrapper.NodeType.PARALLEL }.each{ - echo " - ${it.displayName}" - } - return null; - } - return branch.status.result -} - -// try-finally construct that properly suppresses exceptions thrown in the finally block. -def tryFinally(Closure main, Closure ... finallies) { - def mainFailure = null - try { - main() - } - catch (Throwable t) { - mainFailure = t - throw t - } - finally { - finallies.each {it -> - try { - it() - } - catch (Throwable t) { - if ( mainFailure ) { - mainFailure.addSuppressed( t ) - } - else { - mainFailure = t - } - } - } - } - if ( mainFailure ) { // We may reach here if only the "finally" failed - throw mainFailure - } -} diff --git a/release/release.gradle b/release/release.gradle index 4dd8f448919a..c439f01ecfc5 100644 --- a/release/release.gradle +++ b/release/release.gradle @@ -10,7 +10,6 @@ import groovy.json.JsonSlurper plugins { id "local.module" - id "org.hibernate.orm.build.doc-pub" id "org.hibernate.orm.build.jdks" id "idea" @@ -192,7 +191,7 @@ def stageIntegrationGuideTask = tasks.register( "stageIntegrationGuide", Copy ) dependsOn ":documentation:renderIntegrationGuides" from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/integrationguide" ) } - into layout.buildDirectory.dir( "documentation/integrationguide" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/integrationguide") } def stageQuickstartTask = tasks.register( "stageQuickstart", Copy ) { @@ -202,7 +201,7 @@ def stageQuickstartTask = tasks.register( "stageQuickstart", Copy ) { dependsOn ':documentation:renderGettingStartedGuides' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/quickstart" ) } - into layout.buildDirectory.dir( "documentation/quickstart" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/quickstart") } def stageTopicalGuideTask = tasks.register( "stageTopicalGuide", Copy ) { @@ -212,8 +211,7 @@ def stageTopicalGuideTask = tasks.register( "stageTopicalGuide", Copy ) { dependsOn ':documentation:renderTopicalGuides' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/topical" ) } - into layout.buildDirectory.dir( "documentation/topical" ) - + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/topical") } def stageIntroductionGuideTask = tasks.register( "stageIntroductionGuide", Copy ) { @@ -223,7 +221,7 @@ def stageIntroductionGuideTask = tasks.register( "stageIntroductionGuide", Copy dependsOn ':documentation:renderIntroductionGuides' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/introduction" ) } - into layout.buildDirectory.dir( "documentation/introduction" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/introduction") } def stageQueryGuideTask = tasks.register( "stageQueryGuide", Copy ) { @@ -233,7 +231,7 @@ def stageQueryGuideTask = tasks.register( "stageQueryGuide", Copy ) { dependsOn ':documentation:renderQueryLanguageGuides' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/querylanguage" ) } - into layout.buildDirectory.dir( "documentation/querylanguage" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/querylanguage") } def stageRepositoriesGuideTask = tasks.register( "stageRepositoriesGuide", Copy ) { @@ -243,7 +241,7 @@ def stageRepositoriesGuideTask = tasks.register( "stageRepositoriesGuide", Copy dependsOn ':documentation:renderRepositories' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/repositories" ) } - into layout.buildDirectory.dir( "documentation/repositories" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/repositories") } def stageUserGuideTask = tasks.register( "stageUserGuide", Copy ) { @@ -253,7 +251,7 @@ def stageUserGuideTask = tasks.register( "stageUserGuide", Copy ) { dependsOn ':documentation:renderUserGuides' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/userguide" ) } - into layout.buildDirectory.dir( "documentation/userguide" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/userguide") } @@ -264,7 +262,7 @@ def stageMigrationGuideTask = tasks.register( "stageMigrationGuide", Copy ) { dependsOn ':documentation:renderMigrationGuide' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/migration-guide" ) } - into layout.buildDirectory.dir( "documentation/migration-guide" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/migration-guide") } def stageWhatsNewGuideTask = tasks.register( "stageWhatsNewGuide", Copy ) { @@ -274,11 +272,7 @@ def stageWhatsNewGuideTask = tasks.register( "stageWhatsNewGuide", Copy ) { dependsOn ':documentation:renderWhatsNew' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/whats-new" ) } - into layout.buildDirectory.dir( "documentation/whats-new" ) -} - -tasks.named( "publishMigrationGuide" ).configure { - dependsOn stageMigrationGuideTask + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/whats-new") } def stageIncubationReportTask = tasks.register( "stageIncubationReport", Copy ) { task -> @@ -289,7 +283,7 @@ def stageIncubationReportTask = tasks.register( "stageIncubationReport", Copy ) tasks.stageOrmReports.dependsOn task from project( ":documentation" ).tasks.generateIncubationReport - into layout.buildDirectory.dir( "documentation/incubating" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/incubating") } def stageInternalsReportTask = tasks.register( "stageInternalsReport", Copy ) { task -> @@ -299,7 +293,7 @@ def stageInternalsReportTask = tasks.register( "stageInternalsReport", Copy ) { dependsOn ':documentation:generateInternalsReport' from project( ":documentation" ).tasks.generateInternalsReport - into layout.buildDirectory.dir( "documentation/internals" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/internals") } def stageDeprecationReportTask = tasks.register( "stageDeprecationReport", Copy ) { @@ -309,7 +303,7 @@ def stageDeprecationReportTask = tasks.register( "stageDeprecationReport", Copy dependsOn ':documentation:generateDeprecationReport' from project( ":documentation" ).tasks.generateDeprecationReport - into layout.buildDirectory.dir( "documentation/deprecated" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/deprecated") } def stageLoggingReportTask = tasks.register( "stageLoggingReport", Copy ) { task -> @@ -319,7 +313,7 @@ def stageLoggingReportTask = tasks.register( "stageLoggingReport", Copy ) { task dependsOn ':documentation:renderLoggingReport' from project( ":documentation" ).tasks.renderLoggingReport - into layout.buildDirectory.dir( "documentation/logging" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/logging") } def stageDialectReportTask = tasks.register( "stageDialectReport", Copy ) { task -> @@ -329,7 +323,7 @@ def stageDialectReportTask = tasks.register( "stageDialectReport", Copy ) { task dependsOn ':documentation:renderDialectReport' from project( ":documentation" ).tasks.renderDialectReport - into project.layout.buildDirectory.dir("documentation/dialect") + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/dialect") } def stageOrmReportsTask = tasks.register( "stageOrmReports" ) { @@ -351,7 +345,7 @@ def stageJavadocsTask = tasks.register( "stageJavadocs", Copy ) { dependsOn ':documentation:javadoc' from project( ":documentation" ).tasks.javadoc - into project.layout.buildDirectory.dir("documentation/javadocs") + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/javadocs") } /** @@ -391,33 +385,10 @@ def assembleDocumentationTask = tasks.register( "assembleDocumentation" ) { // * On the published java modules, this means publishing its artifacts to Sonatype // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -def uploadDocumentationTask = tasks.named( "uploadDocumentation" ) { - group = "release-perform" - description = "Uploads assembled documentation to the doc server" - dependsOn assembleDocumentationTask - - doFirst { - if ( ormBuildDetails.hibernateVersion.isSnapshot ) { - logger.error( "Cannot perform upload of SNAPSHOT documentation" ); - throw new RuntimeException( "Cannot perform upload of SNAPSHOT documentation" ); - } - else { - logger.lifecycle( "Uploading documentation ..." ) - } - } - - doLast { - logger.lifecycle( 'Done uploading documentation' ) - } -} - tasks.register( 'releasePerform' ) { group = "release-perform" description "Scripted release 'Release Perform' stage" - dependsOn uploadDocumentationTask - dependsOn uploadDocumentationDescriptor - doFirst { if ( ormBuildDetails.releaseDetails.shouldCreateTag() ) { assert project.gitRemote != null diff --git a/settings.gradle b/settings.gradle index f27540865894..064abb83986a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -182,7 +182,7 @@ dependencyResolutionManagement { def shrinkwrapVersion = version "shrinkwrap", "1.2.6" def shrinkwrapDescriptorsVersion = version "shrinkwrapDescriptors", "2.0.0" def weldVersion = version "weld", "6.0.3.Final" - def wildFlyTxnClientVersion = version "wildFlyTxnClient", "2.0.0.Final" + def wildFlyTxnClientVersion = version "wildFlyTxnClient", "3.0.5.Final" def jfrUnitVersion = version "jfrUnit", "1.0.0.Alpha2" @@ -212,7 +212,7 @@ dependencyResolutionManagement { library( "jbossJta", "org.jboss.narayana.jta", "narayana-jta" ).versionRef( jbossJtaVersion ) library( "jbossTxSpi", "org.jboss", "jboss-transaction-spi" ).versionRef( jbossTxSpiVersion ) - library( "wildFlyTxnClient", "org.wildfly.transaction", "wildfly-transaction-client-jakarta" ).versionRef( wildFlyTxnClientVersion ) + library( "wildFlyTxnClient", "org.wildfly.transaction", "wildfly-transaction-client" ).versionRef( wildFlyTxnClientVersion ) library( "weld", "org.jboss.weld.se", "weld-se-shaded" ).versionRef( weldVersion ) diff --git a/tooling/hibernate-ant/src/main/java/org/hibernate/tool/hbm2ddl/SchemaExport.java b/tooling/hibernate-ant/src/main/java/org/hibernate/tool/hbm2ddl/SchemaExport.java index 44810b3727bf..e6aea405079b 100644 --- a/tooling/hibernate-ant/src/main/java/org/hibernate/tool/hbm2ddl/SchemaExport.java +++ b/tooling/hibernate-ant/src/main/java/org/hibernate/tool/hbm2ddl/SchemaExport.java @@ -390,6 +390,8 @@ public static void execute(CommandLineArgs commandLineArgs) throws Exception { StandardServiceRegistry serviceRegistry = buildStandardServiceRegistry( commandLineArgs ); try { final MetadataImplementor metadata = buildMetadata( commandLineArgs, serviceRegistry ); + metadata.orderColumns( false ); + metadata.validate(); new SchemaExport() .setHaltOnError( commandLineArgs.halt ) diff --git a/tooling/hibernate-gradle-plugin/hibernate-gradle-plugin.gradle b/tooling/hibernate-gradle-plugin/hibernate-gradle-plugin.gradle index 4c893be0e882..84256f4e0dab 100644 --- a/tooling/hibernate-gradle-plugin/hibernate-gradle-plugin.gradle +++ b/tooling/hibernate-gradle-plugin/hibernate-gradle-plugin.gradle @@ -171,19 +171,19 @@ gradle.taskGraph.whenReady { tg -> } // verify credentials for publishing the plugin up front to avoid any work (only if we are publishing) - if ( tg.hasTask( ":publishPlugins" ) && project.tasks.publishPlugins.enabled ) { + if ( tg.hasTask( tasks.publishPlugins ) && project.tasks.publishPlugins.enabled ) { // we are publishing the plugin - make sure there is a credentials pair // // first, check the `GRADLE_PUBLISH_KEY` / `GRADLE_PUBLISH_SECRET` combo (env vars) // and then the `gradle.publish.key` / `gradle.publish.secret` combo (project prop) // - see https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html#account_setup if ( System.getenv().get("GRADLE_PUBLISH_KEY") != null ) { - if ( System.getenv().get("GRADLE_PUBLISH_SECRET") != null ) { + if ( System.getenv().get("GRADLE_PUBLISH_SECRET") == null ) { throw new RuntimeException( "`GRADLE_PUBLISH_KEY` specified, but not `GRADLE_PUBLISH_SECRET` for publishing Gradle plugin" ) } } else if ( project.findProperty( 'gradle.publish.key' ) != null ) { - if ( project.findProperty( 'gradle.publish.secret' ) != null ) { + if ( project.findProperty( 'gradle.publish.secret' ) == null ) { throw new RuntimeException( "`gradle.publish.key` specified, but not `gradle.publish.secret` for publishing Gradle plugin" ) } } diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/constraint/DataTest.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/constraint/DataTest.java new file mode 100644 index 000000000000..2d48397acc58 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/constraint/DataTest.java @@ -0,0 +1,24 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.data.constraint; + +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.jupiter.api.Test; + +import static org.hibernate.processor.test.util.TestUtil.assertMetamodelClassGeneratedFor; +import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString; + +@CompilationTest +class DataTest { + @Test + @WithClasses({MyEntity.class, MyConstrainedRepository.class}) + void test() { + System.out.println( getMetaModelSourceAsString( MyEntity.class ) ); + System.out.println( getMetaModelSourceAsString( MyConstrainedRepository.class ) ); + assertMetamodelClassGeneratedFor( MyEntity.class ); + assertMetamodelClassGeneratedFor( MyConstrainedRepository.class ); + } +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/constraint/MyConstrainedRepository.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/constraint/MyConstrainedRepository.java new file mode 100644 index 000000000000..ee53f05dd965 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/constraint/MyConstrainedRepository.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.data.constraint; + +import jakarta.data.repository.CrudRepository; +import jakarta.data.repository.Find; +import jakarta.data.repository.Repository; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +@Repository +public interface MyConstrainedRepository extends CrudRepository { + + @Valid + @NotNull + @Find + MyEntity findByName(@NotNull @Size(min = 5) String name); +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/constraint/MyEntity.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/constraint/MyEntity.java new file mode 100644 index 000000000000..4b30805e0462 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/constraint/MyEntity.java @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.data.constraint; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class MyEntity { + + @Id + private Long id; + @Column(unique = true) + private String name; +} diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AbstractCriteriaMethod.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AbstractCriteriaMethod.java index 5c8eb7565da3..ceadd53f1d86 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AbstractCriteriaMethod.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AbstractCriteriaMethod.java @@ -80,14 +80,24 @@ String specificationType() { } @Override - void createQuery(StringBuilder declaration) { + void createQuery(StringBuilder declaration, boolean declareVariable) { + if ( declareVariable ) { + if ( dataRepository && !isReactive() ) { + declaration + .append('\t'); + } + declaration + .append('\t'); + declaration + .append("var _select = "); + } final boolean specification = isUsingSpecification(); if ( specification && !isReactive() ) { declaration .append("_spec.createQuery(") .append(localSessionName()) .append(getObjectCall()) - .append(")\n"); + .append(")"); } else { declaration @@ -103,7 +113,7 @@ void createQuery(StringBuilder declaration) { else { declaration.append("_query"); } - declaration.append(")\n"); + declaration.append( ")" ); } } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AbstractQueryMethod.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AbstractQueryMethod.java index 65e43eef6ab6..9405f757fdd6 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AbstractQueryMethod.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AbstractQueryMethod.java @@ -453,10 +453,10 @@ void makeKeyedPage(StringBuilder declaration, List paramTypes) { "\t\t\t\t_results.isFirstPage() ? null : beforeCursor(_cursors.get(0), pageRequest.page()-1, pageRequest.size(), pageRequest.requestTotal()))"; static final String MAKE_KEYED_PAGE - = "\tvar _unkeyedPage =\n" + + = "\t\tvar _unkeyedPage =\n" + "\t\t\tpage(pageRequest.size(), (int) pageRequest.page()-1)\n" + "\t\t\t\t\t.keyedBy(_orders);\n" + - "\tvar _keyedPage =\n" + + "\t\tvar _keyedPage =\n" + "\t\t\tpageRequest.cursor()\n" + "\t\t\t\t\t.map(_cursor -> {\n" + "\t\t\t\t\t\t@SuppressWarnings(\"unchecked\")\n" + @@ -468,22 +468,25 @@ void makeKeyedPage(StringBuilder declaration, List paramTypes) { "\t\t\t\t\t\t};\n" + "\t\t\t\t\t}).orElse(_unkeyedPage);"; - void createQuery(StringBuilder declaration) {} + void createQuery(StringBuilder declaration, boolean declareVariable) {} void createSpecification(StringBuilder declaration) {} - void setParameters(StringBuilder declaration, List paramTypes, String indent) {} + void setParameters(StringBuilder declaration, List paramTypes) {} - void tryReturn(StringBuilder declaration, List paramTypes, @Nullable String containerType) { - if ( isJakartaCursoredPage(containerType) ) { - makeKeyedPage( declaration, paramTypes ); - } + void inTry(StringBuilder declaration) { if ( dataRepository && !isReactive() ) { declaration .append("\ttry {\n"); } - if ( JD_CURSORED_PAGE.equals(containerType) - || JD_PAGE.equals(containerType) ) { + } + + void results(StringBuilder declaration, List paramTypes, @Nullable String containerType) { + if ( isJakartaCursoredPage(containerType) ) { + makeKeyedPage( declaration, paramTypes ); + } + if ( isJakartaCursoredPage(containerType) + || isJakartaPage(containerType) ) { if ( dataRepository ) { declaration .append('\t'); @@ -522,6 +525,11 @@ void tryReturn(StringBuilder declaration, List paramTypes, @Nullable Str } } + void select(StringBuilder declaration) { + declaration + .append("_select\n"); + } + private void totalResults(StringBuilder declaration, List paramTypes) { declaration .append("\tlong _totalResults = \n\t\t\t\t"); @@ -532,8 +540,7 @@ private void totalResults(StringBuilder declaration, List paramTypes) { declaration .append(parameterName(JD_PAGE_REQUEST, paramTypes, paramNames)) .append(".requestTotal()\n\t\t\t\t\t\t? "); - createQuery( declaration ); - setParameters( declaration, paramTypes, "\t\t\t\t\t"); + select( declaration ); if ( isUsingEntityManager() ) { declaration .append("\t\t\t\t\t"); diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java index 123589a1f799..531c0fc5fb43 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java @@ -2064,7 +2064,7 @@ private void createCriteriaFinder( new CriteriaFinderMethod( this, method, methodName, - returnType.toString(), + typeAsString( returnType, false ), containerType, paramNames, paramTypes, @@ -2377,7 +2377,7 @@ && matchesNaturalKey( entity, fieldTypes ) ) { new CriteriaFinderMethod( this, method, methodName, - returnType.toString(), + typeAsString( returnType, false ), containerType, paramNames, paramTypes, @@ -2481,7 +2481,7 @@ private void createSingleParameterFinder( new CriteriaFinderMethod( this, method, methodName, - returnType.toString(), + typeAsString( returnType, false ), containerType, paramNames, paramTypes, @@ -3425,19 +3425,35 @@ private List parameterTypes(ExecutableElement method) { * Workaround for a bug in Java 20/21. Should not be necessary! */ private String typeAsString(TypeMirror type) { - String result = type.toString(); - for ( AnnotationMirror annotation : type.getAnnotationMirrors() ) { - final String annotationString = annotation.toString(); - result = result - // if it has a space after it, we need to remove that too - .replace(annotationString + ' ', "") - // just in case it did not have a space after it - .replace(annotationString, ""); - } - for ( AnnotationMirror annotation : type.getAnnotationMirrors() ) { - result = annotation.toString() + ' ' + result; + return typeAsString( type, true ); + } + + private String typeAsString(TypeMirror type, boolean includeAnnotations) { + if ( type instanceof DeclaredType dt && dt.asElement() instanceof TypeElement te ) { + StringBuilder result = new StringBuilder(); + if ( includeAnnotations ) { + for ( AnnotationMirror annotation : type.getAnnotationMirrors() ) { + result.append( annotation.toString() ).append( ' ' ); + } + } + // get the "fqcn" without any type arguments + result.append( te.getQualifiedName().toString() ); + // add the < ? ,? ....> as necessary: + if ( !dt.getTypeArguments().isEmpty() ) { + result.append( "<" ); + int index = 0; + for ( ; index < dt.getTypeArguments().size() - 1; index++ ) { + result.append( typeAsString( dt.getTypeArguments().get( index ), true ) ) + .append( ", " ); + } + result.append( typeAsString( dt.getTypeArguments().get( index ), true ) ); + result.append( ">" ); + } + return result.toString(); + } + else { + return type.toString(); } - return result; } private TypeMirror parameterType(VariableElement parameter) { diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/CriteriaDeleteMethod.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/CriteriaDeleteMethod.java index ed78a0c4b9b8..58d88fc997b7 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/CriteriaDeleteMethod.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/CriteriaDeleteMethod.java @@ -57,7 +57,7 @@ void executeQuery(StringBuilder declaration, List paramTypes) { createSpecification( declaration ); handleRestrictionParameters( declaration, paramTypes ); tryReturn(declaration); - createQuery( declaration ); + createQuery( declaration, false ); execute( declaration ); } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/CriteriaFinderMethod.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/CriteriaFinderMethod.java index 9545578ce2d1..b646738e94a4 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/CriteriaFinderMethod.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/CriteriaFinderMethod.java @@ -57,9 +57,12 @@ void executeQuery(StringBuilder declaration, List paramTypes) { createSpecification( declaration ); handleRestrictionParameters( declaration, paramTypes ); collectOrdering( declaration, paramTypes, containerType ); - tryReturn( declaration, paramTypes, containerType ); + inTry( declaration ); + createQuery( declaration, true ); + declaration.append( ";\n" ); + results( declaration, paramTypes, containerType ); castResult( declaration ); - createQuery( declaration ); + select( declaration ); handlePageParameters( declaration, paramTypes, containerType ); boolean unwrapped = initiallyUnwrapped(); unwrapped = enableFetchProfile( declaration, unwrapped ); diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/QueryMethod.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/QueryMethod.java index 15770caf2c69..6e330a49f634 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/QueryMethod.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/QueryMethod.java @@ -97,10 +97,13 @@ public String getAttributeDeclarationString() { handleRestrictionParameters( declaration, paramTypes ); collectOrdering( declaration, paramTypes, containerType ); chainSession( declaration ); - tryReturn( declaration, paramTypes, containerType ); + inTry( declaration ); + createQuery( declaration, true ); + setParameters( declaration, paramTypes ); + declaration.append( ";\n" ); + results( declaration, paramTypes, containerType ); castResult( declaration ); - createQuery( declaration ); - setParameters( declaration, paramTypes, ""); + select( declaration ); handlePageParameters( declaration, paramTypes, containerType ); execute( declaration, initiallyUnwrapped() ); convertExceptions( declaration ); @@ -116,7 +119,16 @@ String specificationType() { } @Override - void createQuery(StringBuilder declaration) { + void createQuery(StringBuilder declaration, boolean declareVariable) { + if ( declareVariable ) { + if ( dataRepository && !isReactive() ) { + declaration + .append('\t'); + } + declaration + .append('\t'); + declaration.append("var _select = "); + } if ( isUsingSpecification() ) { if ( isReactive() ) { declaration @@ -130,7 +142,7 @@ void createQuery(StringBuilder declaration) { .append("_spec.createQuery(") .append(localSessionName()) .append(getObjectCall()) - .append(")\n"); + .append(")"); } } else { @@ -147,7 +159,7 @@ void createQuery(StringBuilder declaration) { .append(annotationMetaEntity.importType(returnTypeClass)) .append(".class"); } - declaration.append(")\n"); + declaration.append(")"); } } @@ -225,17 +237,15 @@ else if ( BOOLEAN.equals(returnTypeName) ) { } @Override - void setParameters(StringBuilder declaration, List paramTypes, String indent) { + void setParameters(StringBuilder declaration, List paramTypes) { for ( int i = 0; i < paramNames.size(); i++ ) { if ( !isSpecialParam( paramTypes.get(i) ) ) { final String paramName = paramNames.get(i); final int ordinal = i+1; if ( queryString.contains(":" + paramName) ) { - declaration.append(indent); setNamedParameter( declaration, paramName ); } else if ( queryString.contains("?" + ordinal) ) { - declaration.append(indent); setOrdinalParameter( declaration, ordinal, paramName ); } } @@ -244,20 +254,20 @@ else if ( queryString.contains("?" + ordinal) ) { private static void setOrdinalParameter(StringBuilder declaration, int i, String paramName) { declaration - .append("\t\t\t.setParameter(") + .append("\n\t\t\t.setParameter(") .append(i) .append(", ") .append(paramName) - .append(")\n"); + .append(")"); } private static void setNamedParameter(StringBuilder declaration, String paramName) { declaration - .append("\t\t\t.setParameter(\"") + .append("\n\t\t\t.setParameter(\"") .append(paramName) .append("\", ") .append(paramName) - .append(")\n"); + .append(")"); } // private String returnType() {