You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by jd...@apache.org on 2026/10/02 14:03:28 UTC
(grails-core) branch fix/h7-user-type-comparisons updated: Type non-Comparable comparison values from the property path too
This is an automated email from the ASF dual-hosted git repository.
jdaugherty pushed a commit to branch fix/h7-user-type-comparisons
in repository https://gitbox.apache.org/repos/asf/grails-core.git
The following commit(s) were added to refs/heads/fix/h7-user-type-comparisons by this push:
new 815583a772 Type non-Comparable comparison values from the property path too
815583a772 is described below
commit 815583a77294a888f29b8442697c97daa9e6d5f8
Author: James Daugherty <jd...@jdresources.net>
AuthorDate: Fri Oct 2 10:03:22 2026 -0400
Type non-Comparable comparison values from the property path too
The JPA ordering overloads take only a Comparable value, so a value that is
not Comparable still went through an untyped literal. An ordering comparison
on a UserType whose class is Serializable but not Comparable therefore kept
failing with the Serializable SemanticException.
The public criteria API has no factory for a value typed from a path, so such
a value now goes through SqmCriteriaNodeBuilder#value(value, path), which
types it the same way the JPA value overloads do. A plain literal remains the
fallback when the builder is not Hibernate's node builder.
---
.../orm/hibernate/query/PredicateGenerator.java | 25 ++++-
.../gorm/tests/UserTypeComparisonSpec.groovy | 121 +++++++++++++++++++--
2 files changed, 129 insertions(+), 17 deletions(-)
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java
index 08cb8bdf69..137cb313a2 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java
@@ -37,6 +37,8 @@ import jakarta.persistence.criteria.Subquery;
import org.hibernate.query.criteria.HibernateCriteriaBuilder;
import org.hibernate.query.criteria.JpaSubQuery;
+import org.hibernate.query.sqm.internal.SqmCriteriaNodeBuilder;
+import org.hibernate.query.sqm.tree.expression.SqmExpression;
import org.springframework.core.convert.ConversionService;
@@ -369,22 +371,22 @@ public class PredicateGenerator {
Object value = convertComparisonValue(entity, propertyName, pc.getValue(), fromsByProvider, propertyPath);
return value instanceof Comparable<?> comparable ?
criteriaBuilder.greaterThan((Expression<? extends Comparable>) propertyPath, (Comparable) comparable) :
- criteriaBuilder.greaterThan((Expression<? extends Comparable>) propertyPath, (Expression) asExpression(value));
+ criteriaBuilder.greaterThan((Expression<? extends Comparable>) propertyPath, (Expression) asExpression(value, propertyPath));
} else if (pc instanceof Query.GreaterThanEquals) {
Object value = convertComparisonValue(entity, propertyName, pc.getValue(), fromsByProvider, propertyPath);
return value instanceof Comparable<?> comparable ?
criteriaBuilder.greaterThanOrEqualTo((Expression<? extends Comparable>) propertyPath, (Comparable) comparable) :
- criteriaBuilder.greaterThanOrEqualTo((Expression<? extends Comparable>) propertyPath, (Expression) asExpression(value));
+ criteriaBuilder.greaterThanOrEqualTo((Expression<? extends Comparable>) propertyPath, (Expression) asExpression(value, propertyPath));
} else if (pc instanceof Query.LessThan) {
Object value = convertComparisonValue(entity, propertyName, pc.getValue(), fromsByProvider, propertyPath);
return value instanceof Comparable<?> comparable ?
criteriaBuilder.lessThan((Expression<? extends Comparable>) propertyPath, (Comparable) comparable) :
- criteriaBuilder.lessThan((Expression<? extends Comparable>) propertyPath, (Expression) asExpression(value));
+ criteriaBuilder.lessThan((Expression<? extends Comparable>) propertyPath, (Expression) asExpression(value, propertyPath));
} else if (pc instanceof Query.LessThanEquals) {
Object value = convertComparisonValue(entity, propertyName, pc.getValue(), fromsByProvider, propertyPath);
return value instanceof Comparable<?> comparable ?
criteriaBuilder.lessThanOrEqualTo((Expression<? extends Comparable>) propertyPath, (Comparable) comparable) :
- criteriaBuilder.lessThanOrEqualTo((Expression<? extends Comparable>) propertyPath, (Expression) asExpression(value));
+ criteriaBuilder.lessThanOrEqualTo((Expression<? extends Comparable>) propertyPath, (Expression) asExpression(value, propertyPath));
} else if (pc instanceof Query.In) {
Object value = pc.getValue();
if (value instanceof QueryableCriteria qc) {
@@ -626,8 +628,19 @@ public class PredicateGenerator {
return convertValue(entity, propertyName, value, propertyPath);
}
- private Expression<?> asExpression(Object value) {
- return value instanceof Expression<?> expression ? expression : criteriaBuilder.literal(value);
+ /**
+ * Returns the value as an expression for a comparison overload that takes an expression, which the JPA ordering
+ * overloads need for a value that is not {@link Comparable}. The public criteria API has no factory for a value
+ * typed from a path, so Hibernate's node builder supplies one, typing the value as the JPA value overloads do.
+ */
+ private Expression<?> asExpression(Object value, Expression<?> propertyPath) {
+ if (value instanceof Expression<?> expression) {
+ return expression;
+ }
+ if (criteriaBuilder instanceof SqmCriteriaNodeBuilder nodeBuilder && propertyPath instanceof SqmExpression<?> path) {
+ return nodeBuilder.value(value, path);
+ }
+ return criteriaBuilder.literal(value);
}
public Predicate generate(
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UserTypeComparisonSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UserTypeComparisonSpec.groovy
index 7af79bd0e1..7ca8e0d46a 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UserTypeComparisonSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UserTypeComparisonSpec.groovy
@@ -58,6 +58,7 @@ class UserTypeComparisonSpec extends HibernateGormDatastoreSpec {
then:
item.amount == new UserTypeComparisonAmount(200)
item.rank.level == 2
+ item.grade == new UserTypeComparisonGrade(2)
}
void 'HQL compares the user type property with a parameter'() {
@@ -147,6 +148,53 @@ class UserTypeComparisonSpec extends HibernateGormDatastoreSpec {
'le' | ['low', 'mid']
}
+ void 'criteria #operator compares a user type property whose class is Serializable but not Comparable'() {
+ expect:
+ names(UserTypeComparisonItem.createCriteria().list {
+ "${operator}"('grade', new UserTypeComparisonGrade(2))
+ }) == expected
+
+ where:
+ operator | expected
+ 'eq' | ['mid']
+ 'ne' | ['high', 'low']
+ 'gt' | ['high']
+ 'ge' | ['high', 'mid']
+ 'lt' | ['low']
+ 'le' | ['low', 'mid']
+ }
+
+ void 'dynamic finder #finder compares a user type property whose class is Serializable but not Comparable'() {
+ expect:
+ names(UserTypeComparisonItem."findAllBy${finder}"(new UserTypeComparisonGrade(2))) == expected
+
+ where:
+ finder | expected
+ 'Grade' | ['mid']
+ 'GradeNotEqual' | ['high', 'low']
+ 'GradeGreaterThan' | ['high']
+ 'GradeGreaterThanEquals' | ['high', 'mid']
+ 'GradeLessThan' | ['low']
+ 'GradeLessThanEquals' | ['low', 'mid']
+ }
+
+ void 'where query #description compares a user type property whose class is Serializable but not Comparable'() {
+ given:
+ UserTypeComparisonGrade grade = new UserTypeComparisonGrade(2)
+
+ expect:
+ names(query(grade).list()) == expected
+
+ where:
+ description | query | expected
+ '==' | { UserTypeComparisonGrade g -> UserTypeComparisonItem.where { grade == g } } | ['mid']
+ '!=' | { UserTypeComparisonGrade g -> UserTypeComparisonItem.where { grade != g } } | ['high', 'low']
+ '>' | { UserTypeComparisonGrade g -> UserTypeComparisonItem.where { grade > g } } | ['high']
+ '>=' | { UserTypeComparisonGrade g -> UserTypeComparisonItem.where { grade >= g } } | ['high', 'mid']
+ '<' | { UserTypeComparisonGrade g -> UserTypeComparisonItem.where { grade < g } } | ['low']
+ '<=' | { UserTypeComparisonGrade g -> UserTypeComparisonItem.where { grade <= g } } | ['low', 'mid']
+ }
+
void 'a comparison with property arithmetic still compares against the expression'() {
given:
Closure criteria = { gt('quantity', reorderLevel * 2) }
@@ -160,6 +208,7 @@ class UserTypeComparisonSpec extends HibernateGormDatastoreSpec {
name: name,
amount: new UserTypeComparisonAmount(cents),
rank: new UserTypeComparisonRank(level),
+ grade: new UserTypeComparisonGrade(level),
quantity: quantity,
reorderLevel: reorderLevel
).save(failOnError: true)
@@ -176,12 +225,14 @@ class UserTypeComparisonItem {
String name
UserTypeComparisonAmount amount
UserTypeComparisonRank rank
+ UserTypeComparisonGrade grade
Integer quantity
Integer reorderLevel
static mapping = {
amount type: UserTypeComparisonAmountType
rank type: UserTypeComparisonRankType
+ grade type: UserTypeComparisonGradeType
}
}
@@ -253,36 +304,46 @@ class UserTypeComparisonRank {
}
@CompileStatic
-class UserTypeComparisonRankType implements UserType<UserTypeComparisonRank> {
+@EqualsAndHashCode
+class UserTypeComparisonGrade implements Serializable {
- @Override
- int getSqlType() {
- Types.INTEGER
+ final int level
+
+ UserTypeComparisonGrade(int level) {
+ this.level = level
}
+}
+
+@CompileStatic
+abstract class UserTypeComparisonIntegerType<T> implements UserType<T> {
+
+ protected abstract T fromLevel(int level)
+
+ protected abstract int toLevel(T value)
@Override
- Class<UserTypeComparisonRank> returnedClass() {
- UserTypeComparisonRank
+ int getSqlType() {
+ Types.INTEGER
}
@Override
- UserTypeComparisonRank nullSafeGet(ResultSet rs, int position, WrapperOptions options) throws SQLException {
+ T nullSafeGet(ResultSet rs, int position, WrapperOptions options) throws SQLException {
int level = rs.getInt(position)
- rs.wasNull() ? null : new UserTypeComparisonRank(level)
+ rs.wasNull() ? null : fromLevel(level)
}
@Override
- void nullSafeSet(PreparedStatement st, UserTypeComparisonRank value, int position, WrapperOptions options) throws SQLException {
+ void nullSafeSet(PreparedStatement st, T value, int position, WrapperOptions options) throws SQLException {
if (value == null) {
st.setNull(position, Types.INTEGER)
}
else {
- st.setInt(position, value.level)
+ st.setInt(position, toLevel(value))
}
}
@Override
- UserTypeComparisonRank deepCopy(UserTypeComparisonRank value) {
+ T deepCopy(T value) {
value
}
@@ -291,3 +352,41 @@ class UserTypeComparisonRankType implements UserType<UserTypeComparisonRank> {
false
}
}
+
+@CompileStatic
+class UserTypeComparisonRankType extends UserTypeComparisonIntegerType<UserTypeComparisonRank> {
+
+ @Override
+ Class<UserTypeComparisonRank> returnedClass() {
+ UserTypeComparisonRank
+ }
+
+ @Override
+ protected UserTypeComparisonRank fromLevel(int level) {
+ new UserTypeComparisonRank(level)
+ }
+
+ @Override
+ protected int toLevel(UserTypeComparisonRank value) {
+ value.level
+ }
+}
+
+@CompileStatic
+class UserTypeComparisonGradeType extends UserTypeComparisonIntegerType<UserTypeComparisonGrade> {
+
+ @Override
+ Class<UserTypeComparisonGrade> returnedClass() {
+ UserTypeComparisonGrade
+ }
+
+ @Override
+ protected UserTypeComparisonGrade fromLevel(int level) {
+ new UserTypeComparisonGrade(level)
+ }
+
+ @Override
+ protected int toLevel(UserTypeComparisonGrade value) {
+ value.level
+ }
+}