jdaugherty opened a new pull request, #16479:
URL: https://github.com/apache/grails-core/pull/16479
## Problem
On Hibernate 7, an equality or ordering comparison on a property mapped with
a custom `UserType` fails in dynamic finders, criteria and where queries:
```
org.hibernate.query.SemanticException: Cannot compare left expression of
type 'com.example.Money'
with right expression of type 'java.io.Serializable'
```
```groovy
class Item {
Money amount
static mapping = { amount type: MoneyUserType }
}
Item.findAllByAmountGreaterThan(money) // fails
Item.createCriteria().list { eq 'amount', money } // fails
Item.where { amount <= money }.list() // fails
Item.executeQuery('from Item i where i.amount > :a', [a: money]) // works
```
`eq`, `ne`, `gt`, `ge`, `lt` and `le` are affected. `inList` and `between`
are not.
## Cause
`PredicateGenerator.convertComparisonValue` wrapped every comparison value
in `criteriaBuilder.literal(value)`. Hibernate types an untyped literal from
the value's class. A class it doesn't know, if it implements `Serializable`,
gets the generic `Serializable` type, and `TypecheckUtil.assertComparable` then
rejects comparing it with the `UserType` path. HQL works because a query
parameter is typed from the path it is compared with. Before edbe4f4879 these
comparisons used the JPA overloads that take a value, and those infer the type
the same way.
## Fix
- Plain values are no longer wrapped in a literal. `equal`/`notEqual` (which
already take an `Object`) and
`greaterThan`/`greaterThanOrEqualTo`/`lessThan`/`lessThanOrEqualTo` get the
value itself, so Hibernate infers its type from the property path, the same as
for HQL parameters. This uses only the public criteria API.
- Property arithmetic expressions (`gt('quantity', reorderLevel * 2)`) still
go through the expression overloads.
- A value that is not `Comparable` can't be passed to JPA's ordering
overloads, so it still goes through a literal, as before. This keeps working
for value classes that are not `Serializable`, which Hibernate leaves untyped.
**Known remaining edge:** an ordering comparison (`gt`/`ge`/`lt`/`le`) on a
value class that is `Serializable` but not `Comparable` still fails as before.
`eq`/`ne` work for it. The public `HibernateCriteriaBuilder` has no factory for
a value typed from a path. Closing this edge would mean calling
`SqmCriteriaNodeBuilder#value(Object, SqmExpression)`, which is internal
Hibernate API, so this PR leaves it out.
## Tests
New `UserTypeComparisonSpec` (H7 core) with a `Serializable`, `Comparable`
`UserType` value class covers:
- all six operators through dynamic finders, criteria and where queries
- a detached-criteria `count()`, plus `inList`/`between`
- HQL as the control
- a second `UserType` whose class is neither `Comparable` nor
`Serializable`, covering the literal fallback for all six operators
- a property-arithmetic comparison, covering the expression path
Mutation-checked:
- The spec against the old `PredicateGenerator`: the 19 `Serializable`
user-type comparisons fail with the `SemanticException` above. The rest pass.
- Dropping the non-`Comparable` fallback (always casting to `Comparable`)
fails the 4 ordering comparisons on the second type.
Run locally:
- `UserTypeComparisonSpec`, `PredicateGeneratorSpec`
- the H7 query/criteria specs plus the TCK criteria, finder, where,
detached-criteria, enum, range, negation, association, null and common-types
specs: 745 tests, 0 failures
- `:grails-data-hibernate7-core:codeStyle`
The full module suite was not run locally; CI covers it.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]