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
+    }
+}