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

jamesfredley pushed a commit to branch fix/basic-collection-in-14610
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 5ef6e7bfbcd87266ea856aae39e4a9366acd57b8
Author: James Fredley <[email protected]>
AuthorDate: Wed Feb 25 23:23:36 2026 -0500

    Fix basic collection type 'in' query (#14610)
    
    When a domain has hasMany to a basic type (e.g. hasMany: [schools: String]),
    using 'in'('schools', ['School2']) in criteria queries fails with 'Parameter
    #1 is not set'. The Basic collection is stored in a join table but
    Restrictions.in() generates SQL against the main entity table.
    
    Fix by detecting Basic collection properties in the 'in' methods and
    automatically creating an alias to the collection table, then restricting
    on the 'elements' pseudo-property instead.
    
    Assisted-by: Claude Code <[email protected]>
---
 .../query/AbstractHibernateCriteriaBuilder.java    |  42 +++++++-
 .../gorm/tests/BasicCollectionInQuerySpec.groovy   | 107 +++++++++++++++++++++
 2 files changed, 147 insertions(+), 2 deletions(-)

diff --git 
a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriteriaBuilder.java
 
b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriteriaBuilder.java
index d23650e909..1d30dc92cd 100644
--- 
a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriteriaBuilder.java
+++ 
b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriteriaBuilder.java
@@ -72,6 +72,9 @@ import org.grails.datastore.mapping.query.Query;
 import org.grails.datastore.mapping.query.api.BuildableCriteria;
 import org.grails.datastore.mapping.query.api.QueryableCriteria;
 import org.grails.datastore.mapping.reflect.NameUtils;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.model.PersistentProperty;
+import org.grails.datastore.mapping.model.types.Basic;
 import org.grails.orm.hibernate.AbstractHibernateDatastore;
 
 /**
@@ -1286,7 +1289,17 @@ public abstract class AbstractHibernateCriteriaBuilder 
extends GroovyObjectSuppo
         if (values instanceof List) {
             values = convertArgumentList((List) values);
         }
-        addToCriteria(Restrictions.in(propertyName, values == null ? 
Collections.EMPTY_LIST : values));
+
+        // Handle basic collection types (hasMany to String/Integer/etc.)
+        // These are stored in a separate join table and cannot use simple 
Restrictions.in().
+        // Instead, create an alias to the collection table and restrict on 
'elements'.
+        if (isBasicCollectionProperty(propertyName)) {
+            String alias = propertyName + ALIAS;
+            createAlias(propertyName, alias);
+            addToCriteria(Restrictions.in(alias + ".elements", values == null 
? Collections.EMPTY_LIST : values));
+        } else {
+            addToCriteria(Restrictions.in(propertyName, values == null ? 
Collections.EMPTY_LIST : values));
+        }
         return this;
     }
 
@@ -1331,7 +1344,15 @@ public abstract class AbstractHibernateCriteriaBuilder 
extends GroovyObjectSuppo
         }
 
         propertyName = calculatePropertyName(propertyName);
-        addToCriteria(Restrictions.in(propertyName, values));
+
+        // Handle basic collection types (hasMany to String/Integer/etc.)
+        if (isBasicCollectionProperty(propertyName)) {
+            String alias = propertyName + ALIAS;
+            createAlias(propertyName, alias);
+            addToCriteria(Restrictions.in(alias + ".elements", values));
+        } else {
+            addToCriteria(Restrictions.in(propertyName, values));
+        }
         return this;
     }
 
@@ -1983,6 +2004,23 @@ public abstract class AbstractHibernateCriteriaBuilder 
extends GroovyObjectSuppo
         }
     }
 
+    /**
+     * Checks if the given property name refers to a Basic collection type
+     * (e.g. hasMany: [schools: String]). Basic collections are stored in
+     * a separate join table and require special handling for 'in' queries.
+     */
+    private boolean isBasicCollectionProperty(String propertyName) {
+        if (datastore == null) {
+            return false;
+        }
+        PersistentEntity entity = 
datastore.getMappingContext().getPersistentEntity(targetClass.getName());
+        if (entity == null) {
+            return false;
+        }
+        PersistentProperty property = entity.getPropertyByName(propertyName);
+        return property instanceof Basic;
+    }
+
     /**
      * Returns the criteria instance
      * @return The criteria instance
diff --git 
a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/BasicCollectionInQuerySpec.groovy
 
b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/BasicCollectionInQuerySpec.groovy
new file mode 100644
index 0000000000..5bf852a916
--- /dev/null
+++ 
b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/BasicCollectionInQuerySpec.groovy
@@ -0,0 +1,107 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package grails.gorm.tests
+
+import grails.gorm.annotation.Entity
+import grails.gorm.transactions.Rollback
+import org.grails.orm.hibernate.HibernateDatastore
+import spock.lang.AutoCleanup
+import spock.lang.Issue
+import spock.lang.Shared
+import spock.lang.Specification
+
+/**
+ * Reproduces https://github.com/apache/grails-core/issues/14610
+ *
+ * When a domain has hasMany to a basic type (String), using 'in' on
+ * that collection property in criteria queries fails with
+ * "Parameter #1 is not set".
+ */
+@Rollback
+class BasicCollectionInQuerySpec extends Specification {
+
+    @Shared @AutoCleanup HibernateDatastore datastore =
+        new HibernateDatastore(BcStudent)
+
+    @Issue("https://github.com/apache/grails-core/issues/14610";)
+    def "in query on basic collection type should work"() {
+        given:
+        def s1 = new BcStudent(name: "Alice", email: "[email protected]")
+        s1.addToSchools("School1")
+        s1.addToSchools("School2")
+        s1.save()
+
+        def s2 = new BcStudent(name: "Bob", email: "[email protected]")
+        s2.addToSchools("School2")
+        s2.addToSchools("School3")
+        s2.save()
+
+        def s3 = new BcStudent(name: "Charlie", email: "[email protected]")
+        s3.addToSchools("School3")
+        s3.save(flush: true)
+
+        when:
+        def results = BcStudent.createCriteria().list {
+            'in'('schools', ['School2'])
+            projections {
+                property 'email'
+            }
+        }
+
+        then:
+        results.sort() == ['[email protected]', '[email protected]']
+    }
+
+    def "workaround using createAlias on basic collection"() {
+        given:
+        def s1 = new BcStudent(name: "Alice2", email: "[email protected]")
+        s1.addToSchools("SchoolA")
+        s1.addToSchools("SchoolB")
+        s1.save()
+
+        def s2 = new BcStudent(name: "Bob2", email: "[email protected]")
+        s2.addToSchools("SchoolB")
+        s2.save(flush: true)
+
+        when:
+        def results = BcStudent.createCriteria().list {
+            createAlias("schools", "s")
+            'in'("s.elements", ["SchoolB"])
+            projections {
+                property 'email'
+            }
+        }
+
+        then:
+        results.sort() == ['[email protected]', '[email protected]']
+    }
+}
+
+@Entity
+class BcStudent {
+    String name
+    String email
+
+    static hasMany = [schools: String]
+
+    static constraints = {
+        name blank: false
+        email blank: false
+    }
+}

Reply via email to