You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/04/02 17:54:53 UTC

(grails-core) branch fix/h7-multi-datasource-executequery created (now 8a035be487)

This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a change to branch fix/h7-multi-datasource-executequery
in repository https://gitbox.apache.org/repos/asf/grails-core.git


      at 8a035be487 fix: normalize Groovy GString to String in Hibernate 7 criteria queries

This branch includes the following new commits:

     new 8a035be487 fix: normalize Groovy GString to String in Hibernate 7 criteria queries

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(grails-core) 01/01: fix: normalize Groovy GString to String in Hibernate 7 criteria queries

Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch fix/h7-multi-datasource-executequery
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 8a035be487249bda510eced81853b83c93b51133
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Thu Apr 2 13:54:39 2026 -0400

    fix: normalize Groovy GString to String in Hibernate 7 criteria queries
    
    Hibernate 7's SqmCriteriaNodeBuilder.valueParameter() throws
    HibernateException when encountering Groovy's GStringImpl, unlike
    Hibernate 5 which silently coerced it. Add normalizeValue() to
    PredicateGenerator that converts CharSequence (including GString) to
    String before passing values to JPA Criteria predicates.
    
    Applied to Equals, NotEquals, IdEquals, SizeEquals, SizeNotEquals, and
    Between handlers. Like/ILike already called toString() and were
    unaffected. Removes @PendingFeature from MultipleDataSourceConnectionsSpec
    which now passes all 4 tests.
    
    Assisted-by: OpenCode <op...@opencode.ai>
---
 .../orm/hibernate/query/PredicateGenerator.java    | 25 ++++++++++++++++------
 .../MultipleDataSourceConnectionsSpec.groovy       |  2 --
 2 files changed, 19 insertions(+), 8 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 07fc841883..19e5be9f08 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
@@ -219,11 +219,11 @@ public class PredicateGenerator {
         } else if (pc instanceof Query.Like c) {
             return cb.like((Expression<String>) fullyQualifiedPath, c.getValue().toString());
         } else if (pc instanceof Query.Equals c) {
-            return cb.equal(fullyQualifiedPath, c.getValue());
+            return cb.equal(fullyQualifiedPath, normalizeValue(c.getValue()));
         } else if (pc instanceof Query.NotEquals c) {
-            return cb.or(cb.notEqual(fullyQualifiedPath, c.getValue()), cb.isNull(fullyQualifiedPath));
+            return cb.or(cb.notEqual(fullyQualifiedPath, normalizeValue(c.getValue())), cb.isNull(fullyQualifiedPath));
         } else if (pc instanceof Query.IdEquals c) {
-            return cb.equal(root.get("id"), c.getValue());
+            return cb.equal(root.get("id"), normalizeValue(c.getValue()));
         } else if (pc instanceof Query.GreaterThan c) {
             return cb.gt((Expression<? extends Number>) fullyQualifiedPath, getNumericValue(c));
         } else if (pc instanceof Query.GreaterThanEquals c) {
@@ -233,9 +233,9 @@ public class PredicateGenerator {
         } else if (pc instanceof Query.LessThanEquals c) {
             return cb.le((Expression<? extends Number>) fullyQualifiedPath, getNumericValue(c));
         } else if (pc instanceof Query.SizeEquals c) {
-            return cb.equal(cb.size((Expression) fullyQualifiedPath), c.getValue());
+            return cb.equal(cb.size((Expression) fullyQualifiedPath), normalizeValue(c.getValue()));
         } else if (pc instanceof Query.SizeNotEquals c) {
-            return cb.notEqual(cb.size((Expression) fullyQualifiedPath), c.getValue());
+            return cb.notEqual(cb.size((Expression) fullyQualifiedPath), normalizeValue(c.getValue()));
         } else if (pc instanceof Query.SizeGreaterThan c) {
             return cb.gt(cb.size((Expression) fullyQualifiedPath), getNumericValue(c));
         } else if (pc instanceof Query.SizeGreaterThanEquals c) {
@@ -245,7 +245,7 @@ public class PredicateGenerator {
         } else if (pc instanceof Query.SizeLessThanEquals c) {
             return cb.le(cb.size((Expression) fullyQualifiedPath), getNumericValue(c));
         } else if (pc instanceof Query.Between c) {
-            return cb.between((Expression) fullyQualifiedPath, (Comparable) c.getFrom(), (Comparable) c.getTo());
+            return cb.between((Expression) fullyQualifiedPath, (Comparable) normalizeValue(c.getFrom()), (Comparable) normalizeValue(c.getTo()));
         }
         return null;
     }
@@ -541,6 +541,19 @@ public class PredicateGenerator {
                 ((Query.NotIn) criterion).getSubquery();
     }
 
+    /**
+     * Normalizes a criterion value for use with JPA Criteria API.
+     * Hibernate 7's SqmCriteriaNodeBuilder requires strict Java types and cannot
+     * coerce Groovy types like GString. This method converts CharSequence (including
+     * GString) to String so Hibernate can process the value correctly.
+     */
+    private static Object normalizeValue(Object value) {
+        if (value instanceof CharSequence && !(value instanceof String)) {
+            return value.toString();
+        }
+        return value;
+    }
+
     private Number getNumericValue(Query.PropertyCriterion criterion) {
         Object value = criterion.getValue();
         if (value != null) {
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceConnectionsSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceConnectionsSpec.groovy
index ff0b5b77ab..f592f41ce5 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceConnectionsSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceConnectionsSpec.groovy
@@ -26,7 +26,6 @@ import org.grails.orm.hibernate.HibernateDatastore
 import org.hibernate.Session
 import org.hibernate.dialect.H2Dialect
 import spock.lang.AutoCleanup
-import spock.lang.PendingFeature
 import spock.lang.Shared
 import spock.lang.Specification
 
@@ -119,7 +118,6 @@ class MultipleDataSourceConnectionsSpec extends Specification {
         }
     }
 
-    @PendingFeature(reason = 'executeQuery with named parameters fails on multi-datasource entity in Hibernate 7 - QueryParameterException')
     void "static GORM operations use first non-default datasource for multi datasource entity"() {
         given: "a unique book name"
         def uniqueName = "The Stand ${UUID.randomUUID()}"