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

Reply via email to