Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d7d9e7d
ci: unify functional test CI to run all tests against both Hibernate …
jamesfredley Apr 7, 2026
e2336d9
fix: register h7 autoconfig and remove h5-specific ehcache config
jamesfredley Apr 7, 2026
f587be1
ci: skip h7-incompatible general tests when running with hibernateVer…
jamesfredley Apr 7, 2026
fbfe66b
fix: make gorm and app1 functional tests H7-compatible
borinquenkid Apr 8, 2026
a458c4a
fix(h7): fix 3 H7 GORM bugs — NonUniqueResultException, aggregate ret…
borinquenkid Apr 8, 2026
707c33d
fix(h7): prevent 'two representations of same collection' in addTo/sa…
borinquenkid Apr 9, 2026
a63dcad
fix(test): correct GormCriteriaQueriesSpec to use safe HQL overloads …
borinquenkid Apr 9, 2026
dc7c4a0
address PR feedback: validate hibernateVersion, rename booleans, fix …
jamesfredley Apr 16, 2026
b92f3d5
Merge remote-tracking branch 'origin/8.0.x-hibernate7' into ci/hibern…
jamesfredley Apr 16, 2026
5a97cbe
fix(ci): substitute grails-bom with grails-hibernate7-bom for H7 func…
jamesfredley Apr 16, 2026
5fa3c96
fix(ci): align hibernate to 7.2.7 and fix grails-bom publication path
jamesfredley Apr 16, 2026
3f8eb79
fix(ci): swap project(':grails-bom') for h7, add micronaut-bom to for…
jamesfredley Apr 16, 2026
b59c548
fix(ci): attach grails-hibernate7-bom platform to test project depend…
jamesfredley Apr 16, 2026
904fd76
fix(ci): pin testcontainers BOM in grails-forge-analytics-postgres
jamesfredley Apr 16, 2026
5db69b2
fix(ci): correct testcontainers artifact name to org.testcontainers:p…
jamesfredley Apr 16, 2026
52ba29e
fix(ci): correct testcontainers spock artifact in grails-forge-cli
jamesfredley Apr 16, 2026
cf6d9ce
chore(forge): move testcontainers version to gradle.properties
jamesfredley Apr 16, 2026
634c9e8
Merge branch '8.0.x-hibernate7' into ci/hibernate-matrix-testing
jamesfredley May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,15 +230,17 @@ jobs:
path: grails-forge/tmp1/cli/**/*
if-no-files-found: 'error'
functional:
name: "Functional Tests (Java ${{ matrix.java }}, indy=${{ matrix.indy }})"
name: "Functional Tests (Java ${{ matrix.java }}, Hibernate ${{ matrix.hibernate-version }}, indy=${{ matrix.indy }})"
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
strategy:
fail-fast: false
matrix:
java: [ 21, 25 ]
hibernate-version: [ '5', '7' ]
indy: [ false ]
include:
- java: 21
hibernate-version: '5'
indy: true
runs-on: ubuntu-24.04
steps:
Expand All @@ -258,6 +260,8 @@ jobs:
- name: "🔍 Setup TestLens"
uses: testlens-app/setup-testlens@v1
- name: "🏃 Run Functional Tests"
env:
GITHUB_MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
run: >
./gradlew bootJar check
--continue
Expand All @@ -270,6 +274,8 @@ jobs:
-PskipHibernate5Tests
-PskipHibernate7Tests
-PskipMongodbTests
-PhibernateVersion=${{ matrix.hibernate-version }}
-PskipHibernate${{ matrix.hibernate-version == '5' && '7' || '5' }}Tests
mongodbFunctional:
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
name: "Mongodb Functional Tests (Java ${{ matrix.java }}, MongoDB ${{ matrix.mongodb-version }}, indy=${{ matrix.indy }})"
Expand Down Expand Up @@ -426,15 +432,14 @@ jobs:
name: grails-gradle-artifacts.txt
path: grails-gradle/build/grails-gradle-artifacts.txt
publish:
needs: [ publishGradle, build, functional, hibernate5Functional, mongodbFunctional ]
needs: [ publishGradle, build, functional, mongodbFunctional ]
if: >-
${{ always() &&
github.repository_owner == 'apache' &&
(github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
needs.publishGradle.result == 'success' &&
(needs.build.result == 'success' || needs.build.result == 'skipped') &&
(needs.functional.result == 'success' || needs.functional.result == 'skipped') &&
(needs.hibernate5Functional.result == 'success' || needs.hibernate5Functional.result == 'skipped') &&
(needs.mongodbFunctional.result == 'success' || needs.mongodbFunctional.result == 'skipped')
}}
runs-on: ubuntu-24.04
Expand Down
74 changes: 74 additions & 0 deletions H7_GORM_BUG_REPORT.md
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.
5 changes: 4 additions & 1 deletion dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,10 @@ ext {
'cache-ri-impl.version' : '1.1.1',
'derby.version' : '10.17.1.0',
'hibernate-models.version' : '1.0.1',
'hibernate.version' : '7.2.5.Final',
// Aligned with Spring Boot 4.0.5's managed Hibernate version to avoid a
// version-constraint conflict when consumers import both grails-hibernate7-bom
// (via grails-data-hibernate7) and spring-boot-dependencies (via grails-base-bom).
'hibernate.version' : '7.2.7.Final',
'jandex.version' : '3.2.3',
'liquibase-hibernate.version' : '4.27.0',
'liquibase-test-harness.version': '1.0.11',
Expand Down
70 changes: 69 additions & 1 deletion gradle/functional-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,35 @@ 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.
// Only '5' and '7' are supported; any other value fails the build fast to catch typos.
def targetHibernateVersion = (project.findProperty('hibernateVersion') ?: '5').toString()
if (!(targetHibernateVersion in ['5', '7'])) {
throw new GradleException(
"Unsupported hibernateVersion '${targetHibernateVersion}'. Expected '5' or '7'.")
}
boolean isHibernate5LabeledProject = project.name.startsWith('grails-test-examples-hibernate5')
boolean isHibernate7LabeledProject = project.name.startsWith('grails-test-examples-hibernate7')
boolean isHibernateSpecificProject = isHibernate5LabeledProject || isHibernate7LabeledProject
boolean isMongoProject = project.name.startsWith('grails-test-examples-mongodb')
boolean isGeneralFunctionalTest = !isHibernateSpecificProject && !isMongoProject

Comment thread
jamesfredley marked this conversation as resolved.
// 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',
]

// Redirect the default grails-bom to grails-hibernate7-bom for general functional tests when
// running with -PhibernateVersion=7, so consumers get the Hibernate 7 version constraints
// (hibernate.version=7.2.5.Final and related). The default grails-bom and grails-hibernate5-bom
// ship the Hibernate 5 constraints.
def redirectBomToH7 = isGeneralFunctionalTest && targetHibernateVersion == '7' && !(project.name in h7IncompatibleProjects)

configurations.configureEach {
resolutionStrategy.dependencySubstitution {
// Test projects will often include dependencies from local projects. This will ensure any dependencies
Expand All @@ -32,7 +61,8 @@ configurations.configureEach {
//TODO: This does not handle libraries that are both test fixtures & a libraries like grails-data-mongodb,
// see grails-test-examples-mongodb-base, & grails-test-examples-mongodb-hibernate5 for project() workaround
if (possibleProject.name == 'grails-bom') {
substitute module(substitutedArtifact) using platform(project(':grails-bom'))
def targetBom = redirectBomToH7 ? ':grails-hibernate7-bom' : ':grails-bom'
substitute module(substitutedArtifact) using platform(project(targetBom))
}
else if(possibleProject.name == 'grails-geb') {
def selector = it.variant(module(substitutedArtifact)) { VariantSelectionDetails details ->
Expand All @@ -51,6 +81,44 @@ 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 (redirectBomToH7) {
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'
}
}

// For general functional test projects running against Hibernate 7, attach the Hibernate 7 BOM as a
// platform on the dependency buckets the test projects use. The test project's own build.gradle
// imports platform(project(':grails-bom')) which lacks Hibernate 7 version constraints
// (hibernate-models, jandex, hibernate-tools-orm, etc.). Attaching the H7 BOM here means we don't
// have to edit each test project's build.gradle, and the H7 constraints take precedence because the
// dependency-substitution above already swaps grails-data-hibernate5 -> grails-data-hibernate7 in
// the same path.
if (isGeneralFunctionalTest && targetHibernateVersion == '7' && !(project.name in h7IncompatibleProjects)) {
def addH7Platform = { String confName ->
def conf = configurations.findByName(confName)
if (conf != null) {
conf.dependencies.add(dependencies.platform(dependencies.project(path: ':grails-hibernate7-bom')))
}
}
plugins.withId('java') {
['implementation', 'compileOnly', 'runtimeOnly',
'testImplementation', 'testCompileOnly', 'testRuntimeOnly',
'integrationTestImplementation', 'integrationTestRuntimeOnly',
'testAndDevelopmentOnly'].each(addH7Platform)
}
}

Expand Down
1 change: 1 addition & 0 deletions grails-forge/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ reflectionsVersion=0.10.2
rockerVersion=2.2.1
shadowVersion=8.3.6
spotlessVersion=6.25.0
testcontainersVersion=1.20.4
testRetryVersion=1.6.2
typesafeConfigVersion=1.4.3

Expand Down
2 changes: 2 additions & 0 deletions grails-forge/grails-forge-analytics-postgres/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ group = 'org.apache.grails.forge'

dependencies {

annotationProcessor platform("io.micronaut:micronaut-bom:$micronautVersion")
annotationProcessor 'io.micronaut:micronaut-graal'
annotationProcessor 'io.micronaut.data:micronaut-data-processor'

implementation platform("io.micronaut:micronaut-bom:$micronautVersion")
implementation project(':grails-forge-core')
implementation "com.google.cloud.sql:postgres-socket-factory:$postgresSocketFactoryVersion"
implementation 'io.micronaut.data:micronaut-data-jdbc'
Expand Down
6 changes: 2 additions & 4 deletions grails-test-examples/gorm/grails-app/conf/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,8 @@ grails:
---
hibernate:
cache:
use_second_level_cache: true
provider_class: net.sf.ehcache.hibernate.EhCacheProvider
region:
factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory
use_second_level_cache: false
use_query_cache: false
dataSource:
pooled: true
jmxExport: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,14 @@ grails:
hibernate:
cache:
queries: false
use_second_level_cache: true
use_second_level_cache: false
use_query_cache: false
region.factory_class: 'org.hibernate.cache.ehcache.EhCacheRegionFactory'

endpoints:
jmx:
unique-names: true

dataSource:
pooled: true
jmxExport: true
Expand Down
Loading