ci: run all functional tests against both Hibernate 5.6 and 7.2#15561
ci: run all functional tests against both Hibernate 5.6 and 7.2#15561jamesfredley wants to merge 5 commits into8.0.x-hibernate7from
Conversation
…5 and 7 Replace the separate functional and hibernate5Functional CI jobs with a single functional job that uses a hibernate-version matrix (['5', '7']). This ensures all 20+ general functional tests run against both Hibernate versions without duplicating test projects. Changes: - functional-test-config.gradle: Add -PhibernateVersion property that uses Gradle dependency substitution to redirect grails-data-hibernate5 to grails-data-hibernate7 for general (non-labeled) test projects. Excludes h5-only runtime deps (hibernate-ehcache, jboss-transaction-api) when testing with Hibernate 7. Add skipHibernate5Tests and skipHibernate7Tests properties to the onlyIf block. - gradle.yml: Merge functional and hibernate5Functional into one job with hibernate-version matrix. Each matrix slot skips the opposite version's labeled projects. Update publish job dependencies. Assisted-by: Claude Code <Claude@Claude.ai>
The h7 boot-plugin AutoConfiguration.imports was empty, preventing Spring Boot from discovering HibernateGormAutoConfiguration when grails-data-hibernate7 is on the classpath. This caused GORM has not been initialized correctly errors in functional tests running with Hibernate 7 via dependency substitution. Also remove Hibernate 5-specific EhCache region factory configuration from the gorm and hyphenated functional test application.yml files. EhCache is not available with Hibernate 7, and the second-level cache is not needed by these tests. Assisted-by: Claude Code <Claude@Claude.ai>
…sion=7 Five general functional test projects use Hibernate 5-specific GORM APIs that changed in Hibernate 7. Skip them when running with -PhibernateVersion=7 rather than letting them fail. Their h7-compatible equivalents already exist in grails-test-examples/hibernate7/. Incompatible projects: - app1: HibernateSpec unit test domain class detection differs in h7 - datasources: ChainedTransactionManager commit behavior changed in h7 - gorm: executeUpdate(String) requires Map parameter in h7 - views-functional-tests: depends on h5-specific caching config - scaffolding-fields: integration test context fails under h7 Assisted-by: Claude Code <Claude@Claude.ai>
Hibernate 5 vs 7 - Detailed Differences and Test CoverageGORM API differences between Hibernate 5.6 and 7.2Investigation of the functional test suite revealed these concrete behavioral differences between the two Hibernate versions: 1.
|
| Project | Failure Category | H7 Equivalent |
|---|---|---|
app1 |
HibernateSpec domain class detection (#2) |
No direct equivalent (unit test only) |
datasources |
ChainedTransactionManager behavior (#3) |
No direct equivalent (new test project) |
gorm |
executeUpdate(String) (#1), @RestoreSystemProperties (#4), transaction propagation |
H7 labeled copies cover most scenarios |
views-functional-tests |
JSON/HAL response structure (#7) | No direct equivalent |
scaffolding-fields |
Scaffolding rendering (#8) | No direct equivalent |
28 general projects run under both H5 and H7 via the matrix:
app2, app3, app4, app5, async-events-pubsub-demo, cache, database-cleanup, demo33, exploded, external-configuration, geb, geb-gebconfig, gsp-layout, gsp-sitemesh3, gsp-spring-boot, hyphenated, issue-11102, issue-11767, issue-15228, issue-698-domain-save-npe, issue-views-182, micronaut, micronaut-groovy-only, namespaces, plugins, scaffolding, test-phases, views-functional-tests-plugin
12 h5-labeled projects run only in the H5 slot (as before):
grails-data-service, grails-data-service-multi-datasource, grails-database-per-tenant, grails-hibernate, grails-hibernate-groovy-proxy, grails-multiple-datasources, grails-multitenant-multi-datasource, grails-partitioned-multi-tenancy, grails-schema-per-tenant, issue450, spring-boot-hibernate, standalone-hibernate
12 h7-labeled projects run only in the H7 slot (as before):
Same 12 projects mirrored in grails-test-examples/hibernate7/ with the API adaptations described above.
Also fixed: H7 boot-plugin AutoConfiguration.imports was empty
The file grails-data-hibernate7/boot-plugin/.../AutoConfiguration.imports was empty - Spring Boot never discovered HibernateGormAutoConfiguration when H7 was on the classpath. This is a bug independent of the CI matrix work; it affects any Spring Boot application using grails-data-hibernate7 via autoconfiguration. Fixed in commit e2336d9.
|
@borinquenkid @jdaugherty I ran short on time to wrap this up. It is close. The test differences above should be reviewed for accuracy. |
There was a problem hiding this comment.
Pull request overview
Updates the build and CI configuration so the full functional-test suite is exercised against both Hibernate 5.6 and 7.2, and fixes missing Spring Boot autoconfiguration registration for grails-data-hibernate7.
Changes:
- Consolidates functional-test CI into a single job with a
hibernate-version: [5, 7]matrix and updates publish job dependencies accordingly. - Adds centralized Gradle dependency substitution/exclusions to run general functional test projects against Hibernate 7 without duplicating projects, plus adds skip flags support in functional test gating.
- Registers
HibernateGormAutoConfigurationfor Hibernate 7 Spring Boot autoconfiguration and removes EhCache-specific config from general test example apps.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/gradle.yml |
Merges functional jobs into a Hibernate-version matrix and updates downstream needs/conditions. |
gradle/functional-test-config.gradle |
Adds -PhibernateVersion switching via dependency substitution, exclusions, and new skip flag handling for functional tests. |
grails-data-hibernate7/boot-plugin/.../AutoConfiguration.imports |
Registers Hibernate 7 GORM Boot autoconfiguration so Spring Boot can discover it. |
grails-test-examples/gorm/grails-app/conf/application.yml |
Removes EhCache region/provider configuration and disables 2nd-level/query cache. |
grails-test-examples/hyphenated/grails-app/conf/application.yml |
Removes EhCache region factory configuration and disables 2nd-level cache. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 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 | ||
|
|
There was a problem hiding this comment.
hibernateVersion is 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 a GradleException otherwise) so the build fails fast when an unsupported value is provided.
| // 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
The skipHibernate5Tests / skipHibernate7Tests logic relies on isHibernate5 / isHibernate7 being defined as negated startsWith(...) checks (so isHibernate5 == false actually means “this is an hibernate5-labeled project”). This inverted naming makes the skip conditions hard to reason about and easy to break with future edits. Consider redefining these booleans to match their names (or renaming them to isNotHibernate5Project/etc.) and then update the onlyIf conditions accordingly.
|
Why not duplicate the tests? This assumes the module substitution works in a consistent way. How do we know if there are dependency conflicts - the compile is only going to compile with 1 while the test will potentially run with both. |
|
I wasn't really concerned with the duplication, given 5.6 will be removed in Grails 9 or 10, mostly likely. But in reviewing these, the duplication has allowed drift and we are not testing the same way, which could allow undocumented breaking changes for end apps. Ideally we make these DRY like the TCK. |
- Fix 17 plain executeUpdate('...') calls across 7 specs in
grails-test-examples/gorm to use executeUpdate('...', [:]).
H7's HibernateGormStaticApi rejects plain CharSequence args
(requires either a GString with interpolated params or the
Map overload).
- Add getDomainClasses() override to BookHibernateSpec in app1.
H7's HibernateSpec uses HibernateDatastoreSpringInitializer
which requires explicit domain class declaration; H5 auto-detected
via classpath scanning.
- Remove grails-test-examples-app1 and grails-test-examples-gorm
from h7IncompatibleProjects list — both now run cleanly under
Hibernate 7 via dependency substitution.
Remaining excluded: datasources (ChainedTransactionManager),
views-functional-tests (HAL/JSON diffs), scaffolding-fields
(grails-fields rendering diffs).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
H7
|
| 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.
…urn types, cross-property arithmetic Bug 2: HibernateQueryExecutor.singleResult() now catches both org.hibernate.NonUniqueResultException and jakarta.persistence.NonUniqueResultException (H7 throws the JPA variant; the original catch missed it) and returns the first result instead of propagating. Bug 4: HqlQueryContext.aggregateTargetClass() now returns precise types per function: count() → Long, avg() → Double, sum/min/max() → Number. Previously all aggregates were bound to Long, causing QueryTypeMismatchException in H7's strict SQM type checking. Bug 5: Cross-property arithmetic in where-DSL (e.g. pageCount > price * 10) was silently dropped — the RHS property reference was coerced to a literal. Fixed via: - PropertyReference: Groovy wrapper returned by propertyMissing for numeric properties; *, +, -, / operators produce a PropertyArithmetic value object - PropertyArithmetic: value type carrying (propertyName, Operator, operand) - HibernateDetachedCriteria: H7-only DetachedCriteria subclass that overrides propertyMissing to return PropertyReference for numeric properties, and newInstance() to preserve the subtype through cloning - HibernateGormStaticApi: overrides where/whereLazy/whereAny to use HibernateDetachedCriteria as the closure delegate - PredicateGenerator: resolveNumericExpression() detects PropertyArithmetic and builds cb.prod/sum/diff/quot(path, operand) instead of a literal H5 and MongoDB are unaffected — all new types are confined to grails-data-hibernate7. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🚨 TestLens detected 27 failed tests 🚨Here is what you can do:
Test Summary
🏷️ Commit: a458c4a Test Failures (first 5 of 27)GormCascadeOperationsSpec > test addTo creates bidirectional link (:grails-test-examples-gorm:integrationTest in CI / Functional Tests (Java 17, Hibernate 7, indy=false))GormCascadeOperationsSpec > test belongsTo allows orphan removal (:grails-test-examples-gorm:integrationTest in CI / Functional Tests (Java 17, Hibernate 7, indy=false))GormCascadeOperationsSpec > test dirty checking with associations (:grails-test-examples-gorm:integrationTest in CI / Functional Tests (Java 17, Hibernate 7, indy=false))GormCascadeOperationsSpec > test saving child with belongsTo saves parent reference (:grails-test-examples-gorm:integrationTest in CI / Functional Tests (Java 17, Hibernate 7, indy=false))GormCascadeOperationsSpec > test updating multiple children (:grails-test-examples-gorm:integrationTest in CI / Functional Tests (Java 17, Hibernate 7, indy=false))Muted TestsSelect tests to mute in this pull request:
Reuse successful test results:
Click the checkbox to trigger a rerun:
Learn more about TestLens at testlens.app. |
Summary
Run all functional tests against both Hibernate 5.6 and 7.2 without duplicating test projects, and fix a bug where Hibernate 7's Spring Boot autoconfiguration was never registered.
Changes
1. Unified CI matrix job (
.github/workflows/gradle.yml)Merged the separate
functionalandhibernate5FunctionalCI jobs into a singlefunctionaljob with ahibernate-version: ['5', '7']matrix dimension (same pattern as MongoDB version matrix).Before (2 jobs, no h7 coverage for general tests):
functionalhibernate5FunctionalAfter (1 job, full h5 + h7 coverage):
functionalhibernate-version: ['5', '7']xjava: [17, 21, 25]Each matrix slot:
-PhibernateVersion=5or7to trigger dependency substitution-PskipHibernate7Testsor-PskipHibernate5Teststo exclude the opposite version's labeled projectsUpdated the
publishjobneedsto remove the deletedhibernate5Functional.2. Gradle dependency substitution (
gradle/functional-test-config.gradle)Added a
-PhibernateVersionproperty that usesresolutionStrategy.dependencySubstitutionto redirect Hibernate 5 dependencies to Hibernate 7 for general (non-labeled) functional test projects:grails-data-hibernate5->grails-data-hibernate7grails-data-hibernate5-spring-boot->grails-data-hibernate7-spring-boothibernate-ehcache,jboss-transaction-api) that have no h7 equivalentThis means zero changes to individual test project
build.gradlefiles - the substitution happens centrally.Also added
skipHibernate5TestsandskipHibernate7Testsproperty checks to theonlyIfblock. These were previously only honored by the unit test configs (hibernate5-test-config.gradle/hibernate7-test-config.gradle), not byfunctional-test-config.gradle.3. Bug fix: h7 boot-plugin
AutoConfiguration.importswas emptyThe file
grails-data-hibernate7/boot-plugin/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importswas empty. This meant Spring Boot never discoveredHibernateGormAutoConfigurationwhengrails-data-hibernate7was on the classpath, causingIllegalStateException: GORM has not been initialized correctlyin any application using h7 via autoconfiguration.Fixed by adding the registration entry (matching h5's file):
This is a bug fix independent of the CI matrix work - it affects any Spring Boot application using
grails-data-hibernate7.4. Remove h5-specific EhCache config from general test projects
Removed
org.hibernate.cache.ehcache.EhCacheRegionFactoryreferences fromapplication.ymlin:grails-test-examples/gorm/- hadprovider_classandregion.factory_classpointing to EhCachegrails-test-examples/hyphenated/- hadregion.factory_classpointing to EhCacheEhCache is not available with Hibernate 7, and these tests don't need second-level caching.
Files changed (5 files, +49/-47)
.github/workflows/gradle.ymlgradle/functional-test-config.gradlegrails-data-hibernate7/.../AutoConfiguration.importsHibernateGormAutoConfiguration(was empty)grails-test-examples/gorm/.../application.ymlgrails-test-examples/hyphenated/.../application.ymlLocal usage