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