-
-
Notifications
You must be signed in to change notification settings - Fork 968
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
dc7c4a0
b92f3d5
5a97cbe
5fa3c96
3f8eb79
b59c548
904fd76
5db69b2
52ba29e
cf6d9ce
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 |
|---|---|---|
|
|
@@ -23,9 +23,15 @@ rootProject.subprojects | |
|
|
||
| // 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') | ||
| // 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 | ||
|
|
||
|
|
@@ -38,6 +44,12 @@ List<String> h7IncompatibleProjects = [ | |
| '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 | ||
|
|
@@ -49,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 -> | ||
|
|
@@ -73,7 +86,7 @@ configurations.configureEach { | |
| // 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)) { | ||
| 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') | ||
| } | ||
|
|
@@ -87,14 +100,37 @@ configurations.configureEach { | |
| } | ||
| } | ||
|
|
||
| // 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) | ||
| } | ||
| } | ||
|
|
||
| List<String> debugArguments = [ | ||
| '-Xmx2g', '-Xdebug', '-Xnoagent', '-Djava.compiler=NONE', | ||
| '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005' | ||
| ] | ||
| tasks.withType(Test).configureEach { Test task -> | ||
| boolean isHibernate5 = !project.name.startsWith('grails-test-examples-hibernate5') | ||
| boolean isHibernate7 = !project.name.startsWith('grails-test-examples-hibernate7') | ||
| boolean isMongo = !project.name.startsWith('grails-test-examples-mongodb') | ||
| // Each boolean name describes what the project IS. Positive names, positive semantics. | ||
| boolean isHibernate5Project = project.name.startsWith('grails-test-examples-hibernate5') | ||
| boolean isHibernate7Project = project.name.startsWith('grails-test-examples-hibernate7') | ||
| boolean isMongoTaskProject = project.name.startsWith('grails-test-examples-mongodb') | ||
|
|
||
| onlyIf { | ||
| if (project.hasProperty('skipFunctionalTests')) { | ||
|
|
@@ -106,50 +142,43 @@ tasks.withType(Test).configureEach { Test task -> | |
| return false | ||
| } | ||
|
|
||
| if (project.hasProperty('onlyHibernate5Tests')) { | ||
| if (isHibernate5) { | ||
| return false | ||
| } | ||
| // Only run hibernate5-labeled projects when -PonlyHibernate5Tests is set | ||
| if (project.hasProperty('onlyHibernate5Tests') && !isHibernate5Project) { | ||
| return false | ||
| } | ||
|
|
||
| if (project.hasProperty('onlyHibernate7Tests')) { | ||
| if (isHibernate7) { | ||
| return false | ||
| } | ||
| // Only run hibernate7-labeled projects when -PonlyHibernate7Tests is set | ||
| if (project.hasProperty('onlyHibernate7Tests') && !isHibernate7Project) { | ||
| return false | ||
| } | ||
|
|
||
| // Skip hibernate5-labeled projects when -PskipHibernate5Tests is set | ||
| if (project.hasProperty('skipHibernate5Tests')) { | ||
| if (!isHibernate5) { | ||
| return false | ||
| } | ||
| if (project.hasProperty('skipHibernate5Tests') && isHibernate5Project) { | ||
| return false | ||
| } | ||
|
|
||
| // Skip hibernate7-labeled projects when -PskipHibernate7Tests is set | ||
| if (project.hasProperty('skipHibernate7Tests')) { | ||
| if (!isHibernate7) { | ||
| return false | ||
| } | ||
| if (project.hasProperty('skipHibernate7Tests') && isHibernate7Project) { | ||
| return false | ||
| } | ||
|
Comment on lines
+155
to
+163
|
||
|
|
||
| if (project.hasProperty('onlyMongodbTests')) { | ||
| if (isMongo) { | ||
| return false | ||
| } | ||
| // Only run mongodb-labeled projects when -PonlyMongodbTests is set | ||
| if (project.hasProperty('onlyMongodbTests') && !isMongoTaskProject) { | ||
| return false | ||
| } | ||
|
|
||
| if (project.hasProperty('onlyCoreTests')) { | ||
| return false | ||
| } | ||
|
|
||
| if(project.hasProperty('skipTests')) { | ||
| if (project.hasProperty('skipTests')) { | ||
| return false | ||
| } | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| if (isMongo && project.hasProperty('serializeMongoTests')) { | ||
| if (isMongoTaskProject && project.hasProperty('serializeMongoTests')) { | ||
| // if the developer decides to run a local mongo instance, the tests must be serialized instead of launching containers as needed | ||
| task.outputs.dir rootProject.layout.buildDirectory.dir('mongo-test-serialize') | ||
| } | ||
|
|
||
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.