-
-
Notifications
You must be signed in to change notification settings - Fork 970
ci: run all functional tests against both Hibernate 5.6 and 7.2 #15561
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 8.0.x-hibernate7
Are you sure you want to change the base?
Changes from all commits
d7d9e7d
e2336d9
f587be1
fbfe66b
a458c4a
707c33d
a63dcad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| ## H7 `gorm` Functional Test Failures — Bug Report | ||
|
|
||
| Running `grails-test-examples-gorm` with `-PhibernateVersion=7` produces 13 failures across 4 specs. | ||
| Below are the 5 distinct root causes. | ||
|
|
||
| --- | ||
|
|
||
| ### Bug 1 (Intentional) — `executeQuery` / `executeUpdate` plain String blocked | ||
|
|
||
| | | | | ||
| |---|---| | ||
| | **Tests** | `test basic HQL query`, `test HQL aggregate functions`, `test HQL group by`, `test executeUpdate for bulk operations` | | ||
| | **Spec** | `GormCriteriaQueriesSpec` | | ||
| | **Error** | `UnsupportedOperationException: executeQuery(CharSequence) only accepts a Groovy GString with interpolated parameters` | | ||
|
|
||
| **Description:** H7 intentionally rejects `executeQuery("from Book where inStock = true")` when no parameters are passed. The same tightening was already applied to `executeUpdate`. Callers must use `executeQuery('...', [:])` or a GString with interpolated params. | ||
|
|
||
| > This is by design. The test bodies need to adopt the parameterized form — not a GORM bug. | ||
|
|
||
| --- | ||
|
|
||
| ### Bug 2 — `DetachedCriteria.get()` throws `NonUniqueResultException` instead of returning first result | ||
|
|
||
| | | | | ||
| |---|---| | ||
| | **Test** | `test detached criteria as reusable query` | | ||
| | **Spec** | `GormCriteriaQueriesSpec:454` | | ||
| | **Error** | `jakarta.persistence.NonUniqueResultException: Query did not return a unique result: 2 results were returned` | | ||
|
|
||
| **Description:** H5 `DetachedCriteria.get()` returned the first matching row when multiple rows existed. H7's `AbstractSelectionQuery.getSingleResult()` is now strict and throws if the result is not unique. | ||
|
|
||
| **Expected fix:** `HibernateQueryExecutor.singleResult()` should apply `setMaxResults(1)` before calling `getSingleResult()`, or switch to `getResultList().stream().findFirst()`. | ||
|
|
||
| --- | ||
|
|
||
| ### Bug 3 — `Found two representations of same collection: gorm.Author.books` | ||
|
|
||
| | | | | ||
| |---|---| | ||
| | **Tests** | `test saving child with belongsTo saves parent reference`, `test dirty checking with associations`, `test belongsTo allows orphan removal`, `test updating multiple children`, `test addTo creates bidirectional link` | | ||
| | **Spec** | `GormCascadeOperationsSpec` | | ||
| | **Error** | `HibernateSystemException: Found two representations of same collection: gorm.Author.books` | | ||
|
|
||
| **Description:** H7 enforces stricter collection identity. After `author.addToBooks(book); author.save(flush: true)`, the session contains two references to the same `Author.books` collection, causing a `HibernateException` on flush. H5 tolerated this. | ||
|
|
||
| **Expected fix:** GORM's `addTo*` / cascade-flush path in `grails-data-hibernate7` must synchronize both sides of the bidirectional association and merge/evict stale collection snapshots before flushing. | ||
|
|
||
| --- | ||
|
|
||
| ### Bug 4 — `@Query` aggregate functions fail with type mismatch | ||
|
|
||
| | | | | ||
| |---|---| | ||
| | **Tests** | `test findAveragePrice`, `test findMaxPageCount` | | ||
| | **Spec** | `GormDataServicesSpec` | | ||
| | **Errors** | `Incorrect query result type: query produces 'java.lang.Double' but type 'java.lang.Long' was given` / `query produces 'java.lang.Integer' but type 'java.lang.Long' was given` | | ||
|
|
||
| **Description:** `HibernateHqlQuery.buildQuery()` always calls `session.createQuery(hql, ctx.targetClass())`. For aggregate HQL (`select avg(b.price) ...`, `select max(b.pageCount) ...`), the query does not return an entity, but `ctx.targetClass()` returns the entity class (e.g., `Book`). H7's `SqmQueryImpl` enforces strict result-type alignment — `avg()` produces `Double`, `max(pageCount)` produces `Integer`, neither is coercible to the bound entity type. | ||
|
|
||
| **Expected fix:** `HibernateHqlQuery.buildQuery()` must detect non-entity HQL (aggregates / projections) and call the untyped `session.createQuery(hql)` in those cases, letting GORM handle result casting downstream. | ||
|
|
||
| --- | ||
|
|
||
| ### Bug 5 — `where { pageCount > price * 10 }` fails with `CoercionException` | ||
|
|
||
| | | | | ||
| |---|---| | ||
| | **Test** | `test where query comparing two properties` | | ||
| | **Spec** | `GormWhereQueryAdvancedSpec:175` | | ||
| | **Error** | `org.hibernate.type.descriptor.java.CoercionException: Error coercing value` | | ||
|
|
||
| **Description:** A where-DSL closure comparing an `Integer` property (`pageCount`) to an arithmetic expression involving a `BigDecimal` property (`price * 10`) worked in H5. H7's SQM type system no longer allows implicit coercion between `Integer` and `BigDecimal` in a comparison predicate. | ||
|
|
||
| **Expected fix:** The GORM where-query-to-SQM translator should emit an explicit `CAST` in the SQM tree when the two operands of a comparison have different numeric types. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,23 @@ rootProject.subprojects | |
| .findAll { !(it.name in testProjects) && !(it.name in docProjects) && !(it.name in cliProjects) } | ||
| .each { project.evaluationDependsOn(it.path) } | ||
|
|
||
| // Determine which Hibernate version to use for general functional tests. | ||
| // Pass -PhibernateVersion=7 to run general functional tests against Hibernate 7 instead of 5. | ||
| def targetHibernateVersion = project.findProperty('hibernateVersion') ?: '5' | ||
| boolean isHibernateSpecificProject = project.name.startsWith('grails-test-examples-hibernate5') || | ||
| project.name.startsWith('grails-test-examples-hibernate7') | ||
| boolean isMongoProject = project.name.startsWith('grails-test-examples-mongodb') | ||
| boolean isGeneralFunctionalTest = !isHibernateSpecificProject && !isMongoProject | ||
|
|
||
| // General functional test projects that use Hibernate 5-specific GORM APIs and cannot run | ||
| // under Hibernate 7 via dependency substitution. | ||
| // Their H7-compatible equivalents live in grails-test-examples/hibernate7/. | ||
| List<String> h7IncompatibleProjects = [ | ||
| 'grails-test-examples-datasources', | ||
| 'grails-test-examples-views-functional-tests', | ||
| 'grails-test-examples-scaffolding-fields', | ||
| ] | ||
|
|
||
| configurations.configureEach { | ||
| resolutionStrategy.dependencySubstitution { | ||
| // Test projects will often include dependencies from local projects. This will ensure any dependencies | ||
|
|
@@ -51,6 +68,22 @@ configurations.configureEach { | |
| } | ||
| } | ||
| } | ||
|
|
||
| // For general (non-hibernate-labeled) functional test projects, redirect Hibernate 5 dependencies | ||
| // to Hibernate 7 projects when -PhibernateVersion=7 is set. These rules are added after the loop | ||
| // so they override the default substitutions for the h5 modules. | ||
| // Projects in h7IncompatibleProjects are excluded since they use H5-specific GORM APIs. | ||
| if (isGeneralFunctionalTest && targetHibernateVersion == '7' && !(project.name in h7IncompatibleProjects)) { | ||
| substitute module('org.apache.grails:grails-data-hibernate5') using project(':grails-data-hibernate7') | ||
| substitute module('org.apache.grails:grails-data-hibernate5-spring-boot') using project(':grails-data-hibernate7-spring-boot') | ||
| } | ||
| } | ||
|
|
||
| // Exclude Hibernate 5-specific runtime dependencies when testing general projects with Hibernate 7. | ||
| // These libraries have no Hibernate 7 equivalent and would cause classpath conflicts. | ||
| if (isGeneralFunctionalTest && targetHibernateVersion == '7' && !(project.name in h7IncompatibleProjects)) { | ||
| exclude group: 'org.hibernate', module: 'hibernate-ehcache' | ||
| exclude group: 'org.jboss.spec.javax.transaction', module: 'jboss-transaction-api_1.3_spec' | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -68,6 +101,11 @@ tasks.withType(Test).configureEach { Test task -> | |
| return false | ||
| } | ||
|
|
||
| // Skip projects with known H7 API incompatibilities when running with hibernateVersion=7 | ||
| if (targetHibernateVersion == '7' && project.name in h7IncompatibleProjects) { | ||
| return false | ||
| } | ||
|
|
||
| if (project.hasProperty('onlyHibernate5Tests')) { | ||
| if (isHibernate5) { | ||
| return false | ||
|
|
@@ -80,6 +118,20 @@ tasks.withType(Test).configureEach { Test task -> | |
| } | ||
| } | ||
|
|
||
| // Skip hibernate5-labeled projects when -PskipHibernate5Tests is set | ||
| if (project.hasProperty('skipHibernate5Tests')) { | ||
| if (!isHibernate5) { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| // Skip hibernate7-labeled projects when -PskipHibernate7Tests is set | ||
| if (project.hasProperty('skipHibernate7Tests')) { | ||
| if (!isHibernate7) { | ||
| return false | ||
| } | ||
| } | ||
|
Comment on lines
+121
to
+133
|
||
|
|
||
| if (project.hasProperty('onlyMongodbTests')) { | ||
| if (isMongo) { | ||
| return false | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| org.grails.datastore.gorm.boot.autoconfigure.HibernateGormAutoConfiguration |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
| package org.grails.orm.hibernate | ||
|
|
||
| import groovy.transform.CompileDynamic | ||
|
|
||
| import grails.gorm.DetachedCriteria | ||
| import org.grails.datastore.mapping.model.PersistentProperty | ||
| import org.grails.orm.hibernate.query.PropertyReference | ||
|
|
||
| /** | ||
| * Hibernate-specific subclass of {@link DetachedCriteria} that overrides | ||
| * {@code propertyMissing} to return a {@link PropertyReference} for numeric | ||
| * persistent properties. This enables cross-property arithmetic in where-DSL | ||
| * expressions such as {@code pageCount > price * 10} without touching shared | ||
| * modules (and therefore without affecting H5 or MongoDB backends). | ||
| */ | ||
| @CompileDynamic | ||
| class HibernateDetachedCriteria<T> extends DetachedCriteria<T> { | ||
|
|
||
| HibernateDetachedCriteria(Class<T> targetClass, String alias = null) { | ||
| super(targetClass, alias) | ||
| } | ||
|
|
||
| @Override | ||
| protected HibernateDetachedCriteria<T> newInstance() { | ||
| new HibernateDetachedCriteria<T>(targetClass, alias) | ||
| } | ||
|
|
||
| @Override | ||
| def propertyMissing(String name) { | ||
| PersistentProperty prop = getPersistentEntity()?.getPropertyByName(name) | ||
| if (prop != null && Number.isAssignableFrom(prop.type)) { | ||
| return new PropertyReference(name) | ||
| } | ||
| super.propertyMissing(name) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hibernateVersionis accepted as an arbitrary string and silently falls back to “Hibernate 5 behavior” for any value other than'7'. That can hide typos/misconfiguration (especially for local runs). Consider normalizing + validating the value (e.g., allow only'5'/'7'and throw aGradleExceptionotherwise) so the build fails fast when an unsupported value is provided.