This is an automated email from the ASF dual-hosted git repository. jdaugherty pushed a commit to branch fix/where-queries-embedded in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit aaf840c6b91e8e621bc83691a04a6d26858fd08a Author: James Daugherty <[email protected]> AuthorDate: Fri Jul 10 12:59:19 2026 -0400 Fix embedded where queries --- .../query/AbstractHibernateCriterionAdapter.java | 20 ++++ .../mapping/simple/query/SimpleMapQuery.groovy | 9 +- .../data/testing/tck/domains/ExternalRef.groovy | 26 ++++++ .../data/testing/tck/domains/SystemManaged.groovy | 29 ++++++ .../data/testing/tck/domains/WorkItem.groovy | 30 ++++++ .../tck/tests/WhereQueryEmbeddedSpec.groovy | 102 +++++++++++++++++++++ 6 files changed, 215 insertions(+), 1 deletion(-) diff --git a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriterionAdapter.java b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriterionAdapter.java index 025f5c8f42..c76553ee5c 100644 --- a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriterionAdapter.java +++ b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriterionAdapter.java @@ -34,6 +34,7 @@ import org.hibernate.criterion.Subqueries; import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria; import org.grails.datastore.mapping.model.PersistentEntity; import org.grails.datastore.mapping.model.types.Association; +import org.grails.datastore.mapping.model.types.Embedded; import org.grails.datastore.mapping.query.AssociationQuery; import org.grails.datastore.mapping.query.Query; import org.grails.datastore.mapping.query.api.QueryableCriteria; @@ -188,6 +189,16 @@ public abstract class AbstractHibernateCriterionAdapter { @Override public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Criterion criterion, String alias) { DetachedAssociationCriteria<?> existing = (DetachedAssociationCriteria<?>) criterion; + if (existing.getAssociation() instanceof Embedded) { + Association<?> association = existing.getAssociation(); + String associationName = association.getName(); + if (alias != null) { + associationName = alias + '.' + associationName; + } + Junction conjunction = Restrictions.conjunction(); + applySubCriteriaToJunction(association.getAssociatedEntity(), hibernateQuery, existing.getCriteria(), conjunction, associationName); + return conjunction; + } if (existing.getAlias() == null) { alias = hibernateQuery.handleAssociationQuery(existing.getAssociation(), existing.getCriteria()); } @@ -210,6 +221,15 @@ public abstract class AbstractHibernateCriterionAdapter { public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Criterion criterion, String alias) { AssociationQuery existing = (AssociationQuery) criterion; Junction conjunction = Restrictions.conjunction(); + Association<?> association = existing.getAssociation(); + if (association instanceof Embedded) { + String associationName = association.getName(); + if (alias != null) { + associationName = alias + '.' + associationName; + } + applySubCriteriaToJunction(association.getAssociatedEntity(), hibernateQuery, existing.getCriteria().getCriteria(), conjunction, associationName); + return conjunction; + } String newAlias = hibernateQuery.handleAssociationQuery(existing.getAssociation(), existing.getCriteria().getCriteria()); if (alias == null) { alias = newAlias; diff --git a/grails-data-simple/src/main/groovy/org/grails/datastore/mapping/simple/query/SimpleMapQuery.groovy b/grails-data-simple/src/main/groovy/org/grails/datastore/mapping/simple/query/SimpleMapQuery.groovy index 9c3d473b72..7cbe86954f 100644 --- a/grails-data-simple/src/main/groovy/org/grails/datastore/mapping/simple/query/SimpleMapQuery.groovy +++ b/grails-data-simple/src/main/groovy/org/grails/datastore/mapping/simple/query/SimpleMapQuery.groovy @@ -29,6 +29,7 @@ import org.grails.datastore.mapping.model.PersistentEntity import org.grails.datastore.mapping.model.PersistentProperty import org.grails.datastore.mapping.model.types.Association import org.grails.datastore.mapping.model.types.Custom +import org.grails.datastore.mapping.model.types.Embedded import org.grails.datastore.mapping.model.types.ToOne import org.grails.datastore.mapping.query.AssociationQuery import org.grails.datastore.mapping.query.Query @@ -294,7 +295,13 @@ class SimpleMapQuery extends Query { protected queryAssociation(allEntities, Association association, Closure callable) { allEntities?.findAll { def propertyName = association.name - if (association instanceof ToOne) { + if (association instanceof Embedded) { + def embedded = it.value[propertyName] + if (embedded != null) { + callable.call(embedded) + } + } + else if (association instanceof ToOne) { def id = it.value[propertyName] diff --git a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/ExternalRef.groovy b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/ExternalRef.groovy new file mode 100644 index 0000000000..70aeb0f338 --- /dev/null +++ b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/ExternalRef.groovy @@ -0,0 +1,26 @@ +/* + * 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 org.apache.grails.data.testing.tck.domains + +class ExternalRef { + + String provider + String value +} diff --git a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/SystemManaged.groovy b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/SystemManaged.groovy new file mode 100644 index 0000000000..037ccf7198 --- /dev/null +++ b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/SystemManaged.groovy @@ -0,0 +1,29 @@ +/* + * 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 org.apache.grails.data.testing.tck.domains + +import org.grails.datastore.gorm.GormEntity + +abstract class SystemManaged implements GormEntity { + + ExternalRef extRef1 + + static embedded = ['extRef1'] +} diff --git a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/WorkItem.groovy b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/WorkItem.groovy new file mode 100644 index 0000000000..e5b0dfff6d --- /dev/null +++ b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/WorkItem.groovy @@ -0,0 +1,30 @@ +/* + * 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 org.apache.grails.data.testing.tck.domains + +import grails.persistence.Entity + +@Entity +class WorkItem extends SystemManaged { + + Long id + Long version + String description +} diff --git a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/WhereQueryEmbeddedSpec.groovy b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/WhereQueryEmbeddedSpec.groovy new file mode 100644 index 0000000000..560190a375 --- /dev/null +++ b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/WhereQueryEmbeddedSpec.groovy @@ -0,0 +1,102 @@ +/* + * 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 org.apache.grails.data.testing.tck.tests + +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import org.apache.grails.data.testing.tck.domains.ExternalRef +import org.apache.grails.data.testing.tck.domains.WorkItem +import spock.lang.Issue + +/** + * Tests for where {} queries against properties of embedded components. + */ +class WhereQueryEmbeddedSpec extends GrailsDataTckSpec { + + void setupSpec() { + manager.registerDomainClasses(WorkItem) + } + + private void createWorkItems() { + new WorkItem(description: 'first', extRef1: new ExternalRef(provider: 'SAP', value: 'ABC-123')).save(flush: true, failOnError: true) + new WorkItem(description: 'second', extRef1: new ExternalRef(provider: 'Jira', value: 'XYZ-456')).save(flush: true, failOnError: true) + } + + @Issue('https://github.com/apache/grails-core/issues/15955') + void 'where query with like on inherited embedded component property'() { + given: + createWorkItems() + + when: + String search = 'ABC' + def query = WorkItem.where {} + query = query.where { + extRef1.value =~ "%${search}%" + } + def results = query.list() + + then: + results.size() == 1 + results[0].description == 'first' + } + + @Issue('https://github.com/apache/grails-core/issues/15955') + void 'where query with equals on inherited embedded component property'() { + given: + createWorkItems() + + when: + def results = WorkItem.where { + extRef1.provider == 'SAP' + }.list() + + then: + results.size() == 1 + results[0].description == 'first' + } + + @Issue('https://github.com/apache/grails-core/issues/15955') + void 'where query with conjunction on embedded component property'() { + given: + createWorkItems() + + when: + def results = WorkItem.where { + description == 'first' && extRef1.value =~ '%ABC%' + }.list() + + then: + results.size() == 1 + results[0].description == 'first' + } + + @Issue('https://github.com/apache/grails-core/issues/15955') + void 'where query with disjunction on embedded component property'() { + given: + createWorkItems() + + when: + def results = WorkItem.where { + description == 'none' || extRef1.provider == 'SAP' + }.list() + + then: + results.size() == 1 + results[0].description == 'first' + } +}
