You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/08/12 18:36:30 UTC

(grails-core) branch chore/cleanup-AbstractCriteriaBuilder created (now 377a7f50ef)

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

borinquenkid pushed a change to branch chore/cleanup-AbstractCriteriaBuilder
in repository https://gitbox.apache.org/repos/asf/grails-core.git


      at 377a7f50ef Fix IntelliJ inspection warnings on AbstractCriteriaBuilder

This branch includes the following new commits:

     new 3ccfac7429 Clean up AbstractCriteriaBuilder and add unit test coverage
     new 377a7f50ef Fix IntelliJ inspection warnings on AbstractCriteriaBuilder

The 2 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/02: Clean up AbstractCriteriaBuilder and add unit test coverage

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

borinquenkid pushed a commit to branch chore/cleanup-AbstractCriteriaBuilder
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 3ccfac7429cd702cbefdb74e41282c5b93791d0c
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Wed Aug 12 08:14:20 2026 -0500

    Clean up AbstractCriteriaBuilder and add unit test coverage
    
    AbstractCriteriaBuilder had zero direct unit tests and, since it's abstract,
    can only be exercised through its concrete subclasses. Adds CriteriaBuilderSpec
    in grails-datamapping-core (via grails.gorm.CriteriaBuilder) and a matching
    spec in grails-datamapping-rx (via grails.gorm.rx.CriteriaBuilder), using
    mocked Query/QueryCreator/MappingContext collaborators since the class only
    builds and delegates Query.Criterion objects rather than persisting anything
    itself. Coverage: AbstractCriteriaBuilder 0% -> 99.7% lines / 100% methods;
    CriteriaBuilder (both sync and rx) -> 100% lines / 100% methods.
    
    Along the way:
    - grails.gorm.CriteriaBuilder's cache/readOnly/join(String)/select overrides
      were byte-for-byte duplicates of AbstractCriteriaBuilder's own bodies,
      existing only to narrow the return type from Criteria to BuildableCriteria
      for fluent chaining. Replaced each with a cast-and-delegate to super,
      matching the pattern already used by grails.gorm.rx.DetachedCriteria.
    - Fixed a real, previously-undetected bug in grails.gorm.rx.CriteriaBuilder.count(Map, Closure):
      it assigned the void return of prepareQuery(...) to a local `query` variable,
      which Groovy evaluates as null, shadowing the real query field for the rest
      of the method. Guaranteed NPE on every real call; never caught because the
      module had zero test coverage before being re-enabled.
    - Cleaned up AbstractCriteriaBuilder per PMD (run as a local, temporary
      diagnostic only - not applied to the build): added 47 missing @Override
      annotations, removed a dead initializer, and reordered 4 string comparisons
      to put the known constant first (avoids NPE if the compared value is null).
    
    Co-Authored-By: Claude Sonnet 5 <no...@anthropic.com>
---
 .../main/groovy/grails/gorm/CriteriaBuilder.java   |  12 +-
 .../query/criteria/AbstractCriteriaBuilder.java    |  57 +-
 .../groovy/grails/gorm/CriteriaBuilderSpec.groovy  | 994 +++++++++++++++++++++
 .../groovy/grails/gorm/rx/CriteriaBuilder.groovy   |   2 +-
 .../grails/gorm/rx/CriteriaBuilderSpec.groovy      | 266 ++++++
 5 files changed, 1317 insertions(+), 14 deletions(-)

diff --git a/grails-datamapping-core/src/main/groovy/grails/gorm/CriteriaBuilder.java b/grails-datamapping-core/src/main/groovy/grails/gorm/CriteriaBuilder.java
index f5458518a7..c342f51391 100644
--- a/grails-datamapping-core/src/main/groovy/grails/gorm/CriteriaBuilder.java
+++ b/grails-datamapping-core/src/main/groovy/grails/gorm/CriteriaBuilder.java
@@ -71,20 +71,17 @@ public class CriteriaBuilder<T> extends AbstractCriteriaBuilder implements Build
 
     @Override
     public BuildableCriteria cache(boolean cache) {
-        query.cache(cache);
-        return this;
+        return (BuildableCriteria) super.cache(cache);
     }
 
     @Override
     public BuildableCriteria readOnly(boolean readOnly) {
-        this.readOnly = readOnly;
-        return this;
+        return (BuildableCriteria) super.readOnly(readOnly);
     }
 
     @Override
     public BuildableCriteria join(String property) {
-        query.join(property);
-        return this;
+        return (BuildableCriteria) super.join(property);
     }
 
     @Override
@@ -95,8 +92,7 @@ public class CriteriaBuilder<T> extends AbstractCriteriaBuilder implements Build
 
     @Override
     public BuildableCriteria select(String property) {
-        query.select(property);
-        return this;
+        return (BuildableCriteria) super.select(property);
     }
 
     /**
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java
index 50e7943914..5e5ca3b1db 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java
@@ -119,6 +119,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
         return this;
     }
 
+    @Override
     public Query.ProjectionList id() {
         if (projectionList != null) {
             projectionList.id();
@@ -131,6 +132,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @return The project list
      */
 
+    @Override
     public Query.ProjectionList count() {
         if (projectionList != null) {
             projectionList.count();
@@ -144,6 +146,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param property The name of the property
      * @return The projection list
      */
+    @Override
     public ProjectionList countDistinct(String property) {
         if (projectionList != null) {
             projectionList.countDistinct(property);
@@ -171,6 +174,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return The projection list
      */
+    @Override
     public ProjectionList distinct() {
         if (projectionList != null) {
             projectionList.distinct();
@@ -184,6 +188,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param property The name of the property
      * @return The projection list
      */
+    @Override
     public ProjectionList distinct(String property) {
         if (projectionList != null) {
             projectionList.distinct(property);
@@ -195,6 +200,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * Count the number of records returned
      * @return The project list
      */
+    @Override
     public ProjectionList rowCount() {
         return count();
     }
@@ -204,6 +210,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param name The name of the property
      * @return The projection list
      */
+    @Override
     public ProjectionList property(String name) {
         if (projectionList != null) {
             projectionList.property(name);
@@ -217,6 +224,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param name The name of the property
      * @return The projection list
      */
+    @Override
     public ProjectionList sum(String name) {
         if (projectionList != null) {
             projectionList.sum(name);
@@ -230,6 +238,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param name The name of the property
      * @return The projection list
      */
+    @Override
     public ProjectionList min(String name) {
         if (projectionList != null) {
             projectionList.min(name);
@@ -243,6 +252,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param name The name of the property
      * @return The PropertyProjection instance
      */
+    @Override
     public ProjectionList max(String name) {
         if (projectionList != null) {
             projectionList.max(name);
@@ -256,6 +266,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param name The name of the property
      * @return The PropertyProjection instance
      */
+    @Override
     public ProjectionList avg(String name) {
         if (projectionList != null) {
             projectionList.avg(name);
@@ -304,7 +315,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
                 PersistentEntity previousEntity = persistentEntity;
                 List<Query.Junction> previousLogicalExpressionStack = logicalExpressionStack;
 
-                Query associationQuery = null;
+                Query associationQuery;
                 try {
                     associationQuery = query.createQuery(property.getName());
                     if (associationQuery instanceof AssociationQuery) {
@@ -361,6 +372,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
         return this;
     }
 
+    @Override
     public Criteria idEquals(Object value) {
         addToCriteria(Restrictions.idEq(value));
         return this;
@@ -378,24 +390,28 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
         return this;
     }
 
+    @Override
     public Criteria isEmpty(String propertyName) {
         validatePropertyName(propertyName, "isEmpty");
         addToCriteria(Restrictions.isEmpty(propertyName));
         return this;
     }
 
+    @Override
     public Criteria isNotEmpty(String propertyName) {
         validatePropertyName(propertyName, "isNotEmpty");
         addToCriteria(Restrictions.isNotEmpty(propertyName));
         return this;
     }
 
+    @Override
     public Criteria isNull(String propertyName) {
         validatePropertyName(propertyName, "isNull");
         addToCriteria(Restrictions.isNull(propertyName));
         return this;
     }
 
+    @Override
     public Criteria isNotNull(String propertyName) {
         validatePropertyName(propertyName, "isNotNull");
         addToCriteria(Restrictions.isNotNull(propertyName));
@@ -410,6 +426,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
+    @Override
     public Criteria eq(String propertyName, Object propertyValue) {
         validatePropertyName(propertyName, "eq");
         addToCriteria(Restrictions.eq(propertyName, propertyValue));
@@ -659,6 +676,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
+    @Override
     public Criteria idEq(Object propertyValue) {
         addToCriteria(Restrictions.idEq(propertyValue));
         return this;
@@ -672,6 +690,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
+    @Override
     public Criteria ne(String propertyName, Object propertyValue) {
         validatePropertyName(propertyName, "ne");
         addToCriteria(Restrictions.ne(propertyName, propertyValue));
@@ -687,6 +706,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param finish The end of the range
      * @return A Criterion instance
      */
+    @Override
     public Criteria between(String propertyName, Object start, Object finish) {
         validatePropertyName(propertyName, "between");
         addToCriteria(Restrictions.between(propertyName, start, finish));
@@ -699,6 +719,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param value The value
      * @return The Criterion instance
      */
+    @Override
     public Criteria gte(String property, Object value) {
         validatePropertyName(property, "gte");
         addToCriteria(Restrictions.gte(property, value));
@@ -711,6 +732,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param value The value
      * @return The Criterion instance
      */
+    @Override
     public Criteria ge(String property, Object value) {
         gte(property, value);
         return this;
@@ -722,6 +744,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param value The value
      * @return The Criterion instance
      */
+    @Override
     public Criteria gt(String property, Object value) {
         validatePropertyName(property, "gt");
         addToCriteria(Restrictions.gt(property, value));
@@ -734,6 +757,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param value The value
      * @return The Criterion instance
      */
+    @Override
     public Criteria lte(String property, Object value) {
         validatePropertyName(property, "lte");
         addToCriteria(Restrictions.lte(property, value));
@@ -746,6 +770,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param value The value
      * @return The Criterion instance
      */
+    @Override
     public Criteria le(String property, Object value) {
         lte(property, value);
         return this;
@@ -757,6 +782,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param value The value
      * @return The Criterion instance
      */
+    @Override
     public Criteria lt(String property, Object value) {
         validatePropertyName(property, "lt");
         addToCriteria(Restrictions.lt(property, value));
@@ -771,6 +797,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
+    @Override
     public Criteria like(String propertyName, Object propertyValue) {
         validatePropertyName(propertyName, "like");
         Assert.notNull(propertyValue, "Cannot use like expression with null value");
@@ -786,6 +813,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
+    @Override
     public Criteria ilike(String propertyName, Object propertyValue) {
         validatePropertyName(propertyName, "ilike");
         Assert.notNull(propertyValue, "Cannot use ilike expression with null value");
@@ -801,6 +829,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
+    @Override
     public Criteria rlike(String propertyName, Object propertyValue) {
         validatePropertyName(propertyName, "like");
         Assert.notNull(propertyValue, "Cannot use like expression with null value");
@@ -816,6 +845,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
+    @Override
     public Criteria in(String propertyName, Collection values) {
         validatePropertyName(propertyName, "in");
         Assert.notNull(values, "Cannot use in expression with null values");
@@ -831,6 +861,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
+    @Override
     public Criteria inList(String propertyName, Collection values) {
         in(propertyName, values);
         return this;
@@ -844,6 +875,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
+    @Override
     public Criteria inList(String propertyName, Object[] values) {
         return in(propertyName, Arrays.asList(values));
     }
@@ -856,10 +888,12 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
+    @Override
     public Criteria in(String propertyName, Object[] values) {
         return in(propertyName, Arrays.asList(values));
     }
 
+    @Override
     public Criteria sizeEq(String propertyName, int size) {
         validatePropertyName(propertyName, "sizeEq");
         addToCriteria(Restrictions.sizeEq(propertyName, size));
@@ -867,30 +901,35 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
 
     }
 
+    @Override
     public Criteria sizeGt(String propertyName, int size) {
         validatePropertyName(propertyName, "sizeGt");
         addToCriteria(Restrictions.sizeGt(propertyName, size));
         return this;
     }
 
+    @Override
     public Criteria sizeGe(String propertyName, int size) {
         validatePropertyName(propertyName, "sizeGe");
         addToCriteria(Restrictions.sizeGe(propertyName, size));
         return this;
     }
 
+    @Override
     public Criteria sizeLe(String propertyName, int size) {
         validatePropertyName(propertyName, "sizeLe");
         addToCriteria(Restrictions.sizeLe(propertyName, size));
         return this;
     }
 
+    @Override
     public Criteria sizeLt(String propertyName, int size) {
         validatePropertyName(propertyName, "sizeLt");
         addToCriteria(Restrictions.sizeLt(propertyName, size));
         return this;
     }
 
+    @Override
     public Criteria sizeNe(String propertyName, int size) {
         validatePropertyName(propertyName, "sizeNe");
         addToCriteria(Restrictions.sizeNe(propertyName, size));
@@ -904,6 +943,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param otherPropertyName The other property
      * @return This criteria
      */
+    @Override
     public Criteria eqProperty(String propertyName, String otherPropertyName) {
         validatePropertyName(propertyName, "eqProperty");
         validatePropertyName(otherPropertyName, "eqProperty");
@@ -918,6 +958,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param otherPropertyName The other property
      * @return This criteria
      */
+    @Override
     public Criteria neProperty(String propertyName, String otherPropertyName) {
         validatePropertyName(propertyName, "neProperty");
         validatePropertyName(otherPropertyName, "neProperty");
@@ -933,6 +974,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param otherPropertyName The other property
      * @return This criteria
      */
+    @Override
     public Criteria gtProperty(String propertyName, String otherPropertyName) {
         validatePropertyName(propertyName, "gtProperty");
         validatePropertyName(otherPropertyName, "gtProperty");
@@ -948,6 +990,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param otherPropertyName The other property
      * @return This criteria
      */
+    @Override
     public Criteria geProperty(String propertyName, String otherPropertyName) {
         validatePropertyName(propertyName, "geProperty");
         validatePropertyName(otherPropertyName, "geProperty");
@@ -962,6 +1005,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param otherPropertyName The other property
      * @return This criteria
      */
+    @Override
     public Criteria ltProperty(String propertyName, String otherPropertyName) {
         validatePropertyName(propertyName, "ltProperty");
         validatePropertyName(otherPropertyName, "ltProperty");
@@ -976,6 +1020,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param otherPropertyName The other property
      * @return This criteria
      */
+    @Override
     public Criteria leProperty(String propertyName, String otherPropertyName) {
         validatePropertyName(propertyName, "leProperty");
         validatePropertyName(otherPropertyName, "leProperty");
@@ -989,6 +1034,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param propertyName The property name to order by
      * @return A Order instance
      */
+    @Override
     public Criteria order(String propertyName) {
         Query.Order o = Query.Order.asc(propertyName);
         if (paginationEnabledList) {
@@ -1025,9 +1071,10 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Order instance
      */
+    @Override
     public Criteria order(String propertyName, String direction) {
         Query.Order o;
-        if (direction.equals(CriteriaBuilder.ORDER_DESCENDING)) {
+        if (CriteriaBuilder.ORDER_DESCENDING.equals(direction)) {
             o = Query.Order.desc(propertyName);
         }
         else {
@@ -1069,9 +1116,9 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
     }
 
     private boolean isCriteriaConstructionMethod(String name, Object[] args) {
-        return (name.equals(CriteriaBuilder.ROOT_CALL) ||
-                name.equals(CriteriaBuilder.ROOT_DO_CALL) ||
-                name.equals(CriteriaBuilder.SCROLL_CALL) && args.length == 1 && args[0] instanceof Closure);
+        return (CriteriaBuilder.ROOT_CALL.equals(name) ||
+                CriteriaBuilder.ROOT_DO_CALL.equals(name) ||
+                CriteriaBuilder.SCROLL_CALL.equals(name) && args.length == 1 && args[0] instanceof Closure);
     }
 
     protected void invokeClosureNode(Object args) {
diff --git a/grails-datamapping-core/src/test/groovy/grails/gorm/CriteriaBuilderSpec.groovy b/grails-datamapping-core/src/test/groovy/grails/gorm/CriteriaBuilderSpec.groovy
new file mode 100644
index 0000000000..58e2b7f99c
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/grails/gorm/CriteriaBuilderSpec.groovy
@@ -0,0 +1,994 @@
+/*
+ *  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
+
+import org.grails.datastore.mapping.core.Session
+import org.grails.datastore.mapping.model.MappingContext
+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.query.AssociationQuery
+import org.grails.datastore.mapping.query.Query
+import org.grails.datastore.mapping.query.QueryCreator
+import org.grails.datastore.mapping.query.api.BuildableCriteria
+import org.grails.datastore.mapping.query.api.QueryableCriteria
+import spock.lang.Specification
+import spock.lang.Unroll
+
+/**
+ * Exercises {@link CriteriaBuilder}, and through it its abstract superclass
+ * {@link org.grails.datastore.gorm.query.criteria.AbstractCriteriaBuilder}, which cannot be
+ * instantiated directly. Collaborators (MappingContext/PersistentEntity/QueryCreator/Query) are
+ * mocked since this class's own responsibility is translating DSL calls into Query.Criterion
+ * objects and delegating to a Query, not persistence itself.
+ */
+class CriteriaBuilderSpec extends Specification {
+
+    PersistentProperty idProperty = Stub(PersistentProperty) {
+        getName() >> 'id'
+    }
+    PersistentProperty nameProperty = Stub(PersistentProperty) {
+        getName() >> 'name'
+    }
+    PersistentEntity persistentEntity = Stub(PersistentEntity) {
+        getIdentity() >> idProperty
+        getPropertyByName(_) >> nameProperty
+    }
+    MappingContext mappingContext = Stub(MappingContext) {
+        getPersistentEntity(CriteriaBuilderTestPerson.name) >> persistentEntity
+    }
+    Query query = Mock(Query)
+    QueryCreator queryCreator = Stub(QueryCreator) {
+        createQuery(CriteriaBuilderTestPerson) >> query
+        isSchemaless() >> false
+    }
+
+    CriteriaBuilder<CriteriaBuilderTestPerson> newBuilder() {
+        def criteria = new CriteriaBuilder<CriteriaBuilderTestPerson>(CriteriaBuilderTestPerson, queryCreator, mappingContext)
+        criteria.@query = query
+        criteria
+    }
+
+    void "constructor rejects a null target class"() {
+        when:
+        new CriteriaBuilder(null, queryCreator, mappingContext)
+
+        then:
+        thrown(IllegalArgumentException)
+    }
+
+    void "constructor rejects a null mapping context"() {
+        when:
+        new CriteriaBuilder(CriteriaBuilderTestPerson, queryCreator, null)
+
+        then:
+        thrown(IllegalArgumentException)
+    }
+
+    void "constructor rejects a class the mapping context does not recognise as persistent"() {
+        given:
+        MappingContext unknownContext = Stub(MappingContext) {
+            getPersistentEntity(_) >> null
+        }
+
+        when:
+        new CriteriaBuilder(CriteriaBuilderTestPerson, queryCreator, unknownContext)
+
+        then:
+        IllegalArgumentException e = thrown()
+        e.message.contains(CriteriaBuilderTestPerson.name)
+    }
+
+    void "getTargetClass returns the class the criteria was built for"() {
+        expect:
+        newBuilder().targetClass == CriteriaBuilderTestPerson
+    }
+
+    void "constructing from a Session resolves the mapping context and query creator from it"() {
+        given:
+        Session session = Stub(Session) {
+            getMappingContext() >> mappingContext
+        }
+
+        when:
+        def criteria = new CriteriaBuilder<CriteriaBuilderTestPerson>(CriteriaBuilderTestPerson, session)
+
+        then:
+        criteria.targetClass == CriteriaBuilderTestPerson
+        criteria.session.is(session)
+    }
+
+    void "constructing from a Session and an existing query reuses that query"() {
+        given:
+        Session session = Stub(Session) {
+            getMappingContext() >> mappingContext
+        }
+
+        when:
+        def criteria = new CriteriaBuilder<CriteriaBuilderTestPerson>(CriteriaBuilderTestPerson, session, query)
+
+        then:
+        criteria.query.is(query)
+        criteria.session.is(session)
+    }
+
+    void "setUniqueResult flags a single-result query"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        criteria.setUniqueResult(true)
+
+        then:
+        criteria.uniqueResult
+    }
+
+    void "getQuery returns null before the query has been initialized"() {
+        expect:
+        new CriteriaBuilder<CriteriaBuilderTestPerson>(CriteriaBuilderTestPerson, queryCreator, mappingContext).query == null
+    }
+
+    void "cache delegates to the query and preserves BuildableCriteria for chaining"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        BuildableCriteria result = criteria.cache(true)
+
+        then:
+        1 * query.cache(true)
+        result.is(criteria)
+    }
+
+    void "readOnly sets the readOnly flag and preserves BuildableCriteria for chaining"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        BuildableCriteria result = criteria.readOnly(true)
+
+        then:
+        result.is(criteria)
+        criteria.readOnly
+    }
+
+    void "join(String) delegates to the query and preserves BuildableCriteria for chaining"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        BuildableCriteria result = criteria.join('books')
+
+        then:
+        1 * query.join('books')
+        result.is(criteria)
+    }
+
+    void "join(String, JoinType) delegates to the query with the join type"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        BuildableCriteria result = criteria.join('books', jakarta.persistence.criteria.JoinType.LEFT)
+
+        then:
+        1 * query.join('books', jakarta.persistence.criteria.JoinType.LEFT)
+        result.is(criteria)
+    }
+
+    void "select delegates to the query and preserves BuildableCriteria for chaining"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        BuildableCriteria result = criteria.select('name')
+
+        then:
+        1 * query.select('name')
+        result.is(criteria)
+    }
+
+    @Unroll
+    void "#method(propertyName, value) validates the property and adds a criterion to the query"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria."$method"('name', 'value')
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+
+        where:
+        method << ['eq', 'ne', 'gt', 'ge', 'lt', 'le', 'gte', 'lte', 'like', 'ilike', 'rlike',
+                    'eqProperty', 'neProperty', 'gtProperty', 'geProperty', 'ltProperty', 'leProperty']
+    }
+
+    void "between adds a range criterion to the query"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.between('name', 'a', 'z')
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    @Unroll
+    void "#method(propertyName) adds a criterion to the query with no value"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria."$method"('name')
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+
+        where:
+        method << ['isEmpty', 'isNotEmpty', 'isNull', 'isNotNull']
+    }
+
+    @Unroll
+    void "#method(propertyName, size) adds a size criterion to the query"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria."$method"('name', 1)
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+
+        where:
+        method << ['sizeEq', 'sizeGt', 'sizeGe', 'sizeLe', 'sizeLt', 'sizeNe']
+    }
+
+    void "in(propertyName, Collection) adds an in criterion to the query"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.in('name', ['a', 'b'])
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "inList(propertyName, Collection) delegates to in"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.inList('name', ['a', 'b'])
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "in(propertyName, Object[]) adds an in criterion to the query"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.in('name', ['a', 'b'] as Object[])
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "inList(propertyName, Object[]) delegates to in"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.inList('name', ['a', 'b'] as Object[])
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "idEquals adds an id equality criterion"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.idEquals(1L)
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "idEq adds an id equality criterion"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.idEq(1L)
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "allEq adds an equality conjunction for every entry in the map"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.allEq([name: 'a', id: 1L])
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "exists adds an exists subquery criterion"() {
+        given:
+        def criteria = newBuilder()
+        QueryableCriteria subquery = Stub(QueryableCriteria)
+
+        when:
+        def result = criteria.exists(subquery)
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "notExists adds a not-exists subquery criterion"() {
+        given:
+        def criteria = newBuilder()
+        QueryableCriteria subquery = Stub(QueryableCriteria)
+
+        when:
+        def result = criteria.notExists(subquery)
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    @Unroll
+    void "#method(propertyName, QueryableCriteria) adds a subquery criterion"() {
+        given:
+        def criteria = newBuilder()
+        QueryableCriteria subquery = Stub(QueryableCriteria)
+
+        when:
+        def result = criteria."$method"('name', subquery)
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+
+        where:
+        method << ['eqAll', 'gtAll', 'ltAll', 'geAll', 'leAll', 'gtSome', 'geSome', 'ltSome', 'leSome',
+                    'in', 'inList', 'notIn']
+    }
+
+    @Unroll
+    void "#method(propertyName, Closure) builds a detached criteria subquery"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria."$method"('name', { eq('name', 'nested') })
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+
+        where:
+        method << ['eqAll', 'gtAll', 'ltAll', 'geAll', 'leAll', 'gtSome', 'geSome', 'ltSome', 'leSome',
+                    'in', 'inList', 'notIn']
+    }
+
+    void "and combines the criteria built inside the closure into a single conjunction"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.and {
+            eq('name', 'a')
+            eq('name', 'b')
+        }
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "or combines the criteria built inside the closure into a single disjunction"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.or {
+            eq('name', 'a')
+            eq('name', 'b')
+        }
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "not negates the criteria built inside the closure"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.not {
+            eq('name', 'a')
+        }
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "order(propertyName) orders ascending and applies immediately when pagination is disabled"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.order('name')
+
+        then:
+        1 * query.order(_)
+        result.is(criteria)
+    }
+
+    void "order(Query.Order) applies immediately when pagination is disabled"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.order(Query.Order.asc('name'))
+
+        then:
+        1 * query.order(_)
+        result.is(criteria)
+    }
+
+    void "order(propertyName, direction) orders descending when requested"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.order('name', CriteriaBuilder.ORDER_DESCENDING)
+
+        then:
+        1 * query.order({ Query.Order o -> o.direction == Query.Order.Direction.DESC })
+        result.is(criteria)
+    }
+
+    void "order defers to orderEntries when pagination is enabled"() {
+        given:
+        def criteria = newBuilder()
+        criteria.paginationEnabledList = true
+
+        when:
+        def result = criteria.order('name')
+
+        then:
+        0 * query.order(_)
+        criteria.orderEntries.size() == 1
+        result.is(criteria)
+    }
+
+    void "order(Query.Order) defers to orderEntries when pagination is enabled"() {
+        given:
+        def criteria = newBuilder()
+        criteria.paginationEnabledList = true
+
+        when:
+        def result = criteria.order(Query.Order.asc('name'))
+
+        then:
+        0 * query.order(_)
+        criteria.orderEntries.size() == 1
+        result.is(criteria)
+    }
+
+    void "order(propertyName, direction) orders ascending by default"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.order('name', CriteriaBuilder.ORDER_ASCENDING)
+
+        then:
+        1 * query.order({ Query.Order o -> o.direction == Query.Order.Direction.ASC })
+        result.is(criteria)
+    }
+
+    void "order(propertyName, direction) defers to orderEntries when pagination is enabled"() {
+        given:
+        def criteria = newBuilder()
+        criteria.paginationEnabledList = true
+
+        when:
+        def result = criteria.order('name', CriteriaBuilder.ORDER_DESCENDING)
+
+        then:
+        0 * query.order(_)
+        criteria.orderEntries.size() == 1
+        result.is(criteria)
+    }
+
+    void "projections builds a projection list and evaluates the closure against it"() {
+        given:
+        def criteria = newBuilder()
+        Query.ProjectionList projectionList = Mock(Query.ProjectionList)
+        query.projections() >> projectionList
+
+        when:
+        def result = criteria.projections {
+            id()
+        }
+
+        then:
+        1 * projectionList.id()
+        result.is(projectionList)
+    }
+
+    void "id delegates to the active projection list once projections have been initialized"() {
+        given:
+        def criteria = newBuilder()
+        Query.ProjectionList projectionList = Mock(Query.ProjectionList)
+        query.projections() >> projectionList
+        criteria.projections {}
+
+        when:
+        criteria.id()
+
+        then:
+        1 * projectionList.id()
+    }
+
+    void "count delegates to the active projection list once projections have been initialized"() {
+        given:
+        def criteria = newBuilder()
+        Query.ProjectionList projectionList = Mock(Query.ProjectionList)
+        query.projections() >> projectionList
+        criteria.projections {}
+
+        when:
+        criteria.count()
+
+        then:
+        1 * projectionList.count()
+    }
+
+    void "distinct() delegates to the active projection list once projections have been initialized"() {
+        given:
+        def criteria = newBuilder()
+        Query.ProjectionList projectionList = Mock(Query.ProjectionList)
+        query.projections() >> projectionList
+        criteria.projections {}
+
+        when:
+        criteria.distinct()
+
+        then:
+        1 * projectionList.distinct()
+    }
+
+    @Unroll
+    void "#method delegates to the active projection list once projections have been initialized"() {
+        given:
+        def criteria = newBuilder()
+        Query.ProjectionList projectionList = Mock(Query.ProjectionList)
+        query.projections() >> projectionList
+        criteria.projections {}
+
+        when:
+        criteria."$method"(*args)
+
+        then:
+        1 * projectionList."$method"(*args)
+
+        where:
+        method           | args
+        'countDistinct'  | ['name']
+        'groupProperty'  | ['name']
+        'distinct'       | ['name']
+        'property'       | ['name']
+        'sum'            | ['name']
+        'min'            | ['name']
+        'max'            | ['name']
+        'avg'            | ['name']
+    }
+
+    void "rowCount delegates to the count projection"() {
+        given:
+        def criteria = newBuilder()
+        Query.ProjectionList projectionList = Mock(Query.ProjectionList)
+        query.projections() >> projectionList
+        criteria.projections {}
+
+        when:
+        criteria.rowCount()
+
+        then:
+        1 * projectionList.count()
+    }
+
+    @Unroll
+    void "#method projection accessor returns null before projections have been initialized"() {
+        given:
+        def criteria = newBuilder()
+
+        expect:
+        criteria."$method"(*args) == null
+
+        where:
+        method          | args
+        'id'            | []
+        'count'         | []
+        'countDistinct' | ['name']
+        'groupProperty' | ['name']
+        'distinct'      | []
+        'distinct'      | ['name']
+        'property'      | ['name']
+        'sum'           | ['name']
+        'min'           | ['name']
+        'max'           | ['name']
+        'avg'           | ['name']
+    }
+
+    void "build invokes the closure against the criteria delegate"() {
+        given:
+        def criteria = newBuilder()
+        boolean invoked = false
+
+        when:
+        criteria.build {
+            invoked = true
+            eq('name', 'a')
+        }
+
+        then:
+        invoked
+        1 * query.add(_)
+    }
+
+    void "build tolerates a null closure"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        criteria.build(null)
+
+        then:
+        noExceptionThrown()
+    }
+
+    void "calling a criteria construction method executes the closure and returns a list by default"() {
+        given:
+        def criteria = newBuilder()
+        query.list() >> ['result']
+
+        when:
+        def result = criteria.call { eq('name', 'a') }
+
+        then:
+        result == ['result']
+    }
+
+    void "calling a criteria construction method returns a single result when uniqueResult is set"() {
+        given:
+        def criteria = newBuilder()
+        query.singleResult() >> 'single'
+
+        when:
+        def result = criteria.call {
+            uniqueResult = true
+            eq('name', 'a')
+        }
+
+        then:
+        result == 'single'
+    }
+
+    void "an unrecognised property access on the criteria delegates to a matching query method"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.max(10)
+
+        then:
+        1 * query.max(10)
+        result.is(query.max(10))
+    }
+
+    void "invoking an association name with a closure builds a nested association query"() {
+        given:
+        Association association = Stub(Association) {
+            getName() >> 'books'
+            getAssociatedEntity() >> persistentEntity
+        }
+        PersistentEntity ownerEntity = Stub(PersistentEntity) {
+            getIdentity() >> idProperty
+            getPropertyByName('books') >> association
+        }
+        MappingContext ownerMappingContext = Stub(MappingContext) {
+            getPersistentEntity(CriteriaBuilderTestPerson.name) >> ownerEntity
+        }
+        AssociationQuery associationQuery = Mock(AssociationQuery)
+        query.createQuery('books') >> associationQuery
+        def criteria = new CriteriaBuilder<CriteriaBuilderTestPerson>(CriteriaBuilderTestPerson, queryCreator, ownerMappingContext)
+
+        when:
+        criteria.books {}
+
+        then:
+        1 * query.add(associationQuery)
+    }
+
+    void "an unresolvable method call throws a MissingMethodException"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        criteria.thisMethodDoesNotExistAnywhere()
+
+        then:
+        thrown(MissingMethodException)
+    }
+
+    void "an unresolvable single-argument non-closure call throws a MissingMethodException"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        criteria.thisMethodDoesNotExistAnywhere('not a closure')
+
+        then:
+        thrown(MissingMethodException)
+    }
+
+    void "invoking a name with a closure that is not an association throws a MissingMethodException"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        criteria.notAnAssociation {}
+
+        then:
+        thrown(MissingMethodException)
+    }
+
+    void "validatePropertyName resolves the identity property when the property is not otherwise found"() {
+        given:
+        PersistentEntity entityWithoutNameLookup = Stub(PersistentEntity) {
+            getIdentity() >> idProperty
+            getPropertyByName('id') >> null
+        }
+        MappingContext idOnlyMappingContext = Stub(MappingContext) {
+            getPersistentEntity(CriteriaBuilderTestPerson.name) >> entityWithoutNameLookup
+        }
+        def criteria = new CriteriaBuilder<CriteriaBuilderTestPerson>(CriteriaBuilderTestPerson, queryCreator, idOnlyMappingContext)
+        criteria.@query = query
+
+        when:
+        def result = criteria.eq('id', 1L)
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "validatePropertyName throws when the property cannot be resolved and the datastore is not schemaless"() {
+        given:
+        PersistentEntity entityWithNoProperties = Stub(PersistentEntity) {
+            getIdentity() >> idProperty
+            getPropertyByName(_) >> null
+        }
+        MappingContext emptyMappingContext = Stub(MappingContext) {
+            getPersistentEntity(CriteriaBuilderTestPerson.name) >> entityWithNoProperties
+        }
+        def criteria = new CriteriaBuilder<CriteriaBuilderTestPerson>(CriteriaBuilderTestPerson, queryCreator, emptyMappingContext)
+        criteria.@query = query
+
+        when:
+        criteria.eq('missing', 1L)
+
+        then:
+        thrown(IllegalArgumentException)
+    }
+
+    void "validatePropertyName rejects a null property name"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        criteria.eq(null, 'value')
+
+        then:
+        thrown(IllegalArgumentException)
+    }
+
+    void "a closure passed as a criterion value is converted to a detached criteria subquery"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria.eq('name', { eq('name', 'nested') })
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "addToCriteria lazily initializes the query when it has not been set yet"() {
+        given:
+        def criteria = new CriteriaBuilder<CriteriaBuilderTestPerson>(CriteriaBuilderTestPerson, queryCreator, mappingContext)
+
+        when:
+        def result = criteria.eq('name', 'value')
+
+        then:
+        criteria.query.is(query)
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "the doCall construction method is recognised in addition to call"() {
+        given:
+        def criteria = newBuilder()
+        query.list() >> ['a']
+
+        when:
+        def result = criteria.doCall { eq('name', 'a') }
+
+        then:
+        result == ['a']
+    }
+
+    void "validatePropertyName tolerates an unresolvable property when the datastore is schemaless"() {
+        given:
+        PersistentEntity entityWithNoProperties = Stub(PersistentEntity) {
+            getIdentity() >> idProperty
+            getPropertyByName(_) >> null
+        }
+        MappingContext emptyMappingContext = Stub(MappingContext) {
+            getPersistentEntity(CriteriaBuilderTestPerson.name) >> entityWithNoProperties
+        }
+        QueryCreator schemalessQueryCreator = Stub(QueryCreator) {
+            createQuery(CriteriaBuilderTestPerson) >> query
+            isSchemaless() >> true
+        }
+        def criteria = new CriteriaBuilder<CriteriaBuilderTestPerson>(CriteriaBuilderTestPerson, schemalessQueryCreator, emptyMappingContext)
+        criteria.@query = query
+
+        when:
+        def result = criteria.eq('missing', 1L)
+
+        then:
+        1 * query.add(_)
+        result.is(criteria)
+    }
+
+    void "list(Closure) evaluates the closure and returns the query's results"() {
+        given:
+        def criteria = newBuilder()
+        query.list() >> ['a', 'b']
+
+        when:
+        def result = criteria.list { eq('name', 'a') }
+
+        then:
+        result == ['a', 'b']
+        1 * query.add(_)
+    }
+
+    void "get(Closure) evaluates the closure and returns a single result"() {
+        given:
+        def criteria = newBuilder()
+        query.singleResult() >> 'single'
+
+        when:
+        def result = criteria.get { eq('name', 'a') }
+
+        then:
+        result == 'single'
+        criteria.uniqueResult
+        1 * query.add(_)
+    }
+
+    void "listDistinct(Closure) applies a distinct projection before listing"() {
+        given:
+        def criteria = newBuilder()
+        Query.ProjectionList projectionList = Mock(Query.ProjectionList)
+        query.projections() >> projectionList
+        query.list() >> ['a']
+
+        when:
+        def result = criteria.listDistinct { eq('name', 'a') }
+
+        then:
+        result == ['a']
+        1 * projectionList.distinct()
+        1 * query.add(_)
+    }
+
+    void "list(Map, Closure) enables pagination, applies ordering and returns a PagedResultList"() {
+        given:
+        def criteria = newBuilder()
+        query.getEntity() >> persistentEntity
+        persistentEntity.getMappingContext() >> mappingContext
+
+        when:
+        def result = criteria.list([:]) { order('name') }
+
+        then:
+        result instanceof PagedResultList
+        criteria.paginationEnabledList
+        1 * query.order(_)
+    }
+
+    void "count(Closure) applies a count projection and returns a single result"() {
+        given:
+        def criteria = newBuilder()
+        Query.ProjectionList projectionList = Mock(Query.ProjectionList)
+        query.projections() >> projectionList
+        query.singleResult() >> 5
+
+        when:
+        def result = criteria.count { eq('name', 'a') }
+
+        then:
+        result == 5
+        criteria.uniqueResult
+        1 * projectionList.count()
+        1 * query.add(_)
+    }
+
+    void "scroll executes the closure as a criteria construction call and returns the results"() {
+        given:
+        def criteria = newBuilder()
+        query.list() >> ['a']
+
+        when:
+        def result = criteria.scroll { eq('name', 'a') }
+
+        then:
+        result == ['a']
+        1 * query.add(_)
+    }
+}
+
+class CriteriaBuilderTestPerson {
+    Long id
+    String name
+}
diff --git a/grails-datamapping-rx/src/main/groovy/grails/gorm/rx/CriteriaBuilder.groovy b/grails-datamapping-rx/src/main/groovy/grails/gorm/rx/CriteriaBuilder.groovy
index 227e434ba6..3baa9342ca 100644
--- a/grails-datamapping-rx/src/main/groovy/grails/gorm/rx/CriteriaBuilder.groovy
+++ b/grails-datamapping-rx/src/main/groovy/grails/gorm/rx/CriteriaBuilder.groovy
@@ -140,7 +140,7 @@ class CriteriaBuilder<T> extends AbstractCriteriaBuilder {
      * @return The total results
      */
     Observable<Number> count(Map args, @DelegatesTo(CriteriaBuilder) Closure additionalCriteria = null) {
-        Query query = prepareQuery(args, additionalCriteria)
+        prepareQuery(args, additionalCriteria)
         query.projections().count()
         return ((RxQuery)query).singleResult(args)
     }
diff --git a/grails-datamapping-rx/src/test/groovy/grails/gorm/rx/CriteriaBuilderSpec.groovy b/grails-datamapping-rx/src/test/groovy/grails/gorm/rx/CriteriaBuilderSpec.groovy
new file mode 100644
index 0000000000..da66bf2322
--- /dev/null
+++ b/grails-datamapping-rx/src/test/groovy/grails/gorm/rx/CriteriaBuilderSpec.groovy
@@ -0,0 +1,266 @@
+/*
+ *  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.rx
+
+import org.grails.datastore.mapping.model.MappingContext
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.model.PersistentProperty
+import org.grails.datastore.mapping.query.Query
+import org.grails.datastore.mapping.query.QueryCreator
+import org.grails.datastore.rx.query.RxQuery
+import rx.Observable
+import spock.lang.Specification
+
+/**
+ * Exercises {@link CriteriaBuilder}'s own reactive terminal operations. The shared query-DSL
+ * methods it inherits from AbstractCriteriaBuilder (eq, gt, and/or/not, projections, etc.) are
+ * already fully covered by grails.gorm.CriteriaBuilderSpec in grails-datamapping-core, against
+ * the same base class -- coverage there applies regardless of which subclass exercises it, so
+ * this spec only needs to cover the methods declared directly on this class.
+ */
+class CriteriaBuilderSpec extends Specification {
+
+    PersistentProperty idProperty = Stub(PersistentProperty) {
+        getName() >> 'id'
+    }
+    PersistentProperty nameProperty = Stub(PersistentProperty) {
+        getName() >> 'name'
+    }
+    PersistentEntity persistentEntity = Stub(PersistentEntity) {
+        getIdentity() >> idProperty
+        getPropertyByName(_) >> nameProperty
+    }
+    MappingContext mappingContext = Stub(MappingContext) {
+        getPersistentEntity(CriteriaBuilderTestPerson.name) >> persistentEntity
+    }
+    Query query = Mock(Query, additionalInterfaces: [RxQuery])
+    QueryCreator queryCreator = Stub(QueryCreator) {
+        createQuery(CriteriaBuilderTestPerson) >> query
+        isSchemaless() >> false
+    }
+
+    def setup() {
+        // Field initializers run top-to-bottom, so this mutual reference has to be wired up
+        // after both fields exist rather than inside either Stub() block.
+        persistentEntity.getMappingContext() >> mappingContext
+        query.getEntity() >> persistentEntity
+    }
+
+    CriteriaBuilder<CriteriaBuilderTestPerson> newBuilder() {
+        def criteria = new CriteriaBuilder<CriteriaBuilderTestPerson>(CriteriaBuilderTestPerson, queryCreator, mappingContext)
+        criteria.@query = query
+        criteria
+    }
+
+    void "get(Closure) evaluates the closure, flags a unique result and returns a single observable"() {
+        given:
+        def criteria = newBuilder()
+        Observable<CriteriaBuilderTestPerson> observable = Observable.just(new CriteriaBuilderTestPerson())
+        ((RxQuery) query).singleResult() >> observable
+
+        when:
+        def result = criteria.get { eq('name', 'a') }
+
+        then:
+        result.is(observable)
+        criteria.uniqueResult
+        1 * query.add(_)
+    }
+
+    void "get() flags a unique result and returns a single observable without evaluating a closure"() {
+        given:
+        def criteria = newBuilder()
+        Observable<CriteriaBuilderTestPerson> observable = Observable.just(new CriteriaBuilderTestPerson())
+        query.singleResult() >> observable
+
+        when:
+        def result = criteria.get()
+
+        then:
+        result.is(observable)
+        criteria.uniqueResult
+    }
+
+    void "find(Closure) delegates to get(Closure)"() {
+        given:
+        def criteria = newBuilder()
+        Observable<CriteriaBuilderTestPerson> observable = Observable.just(new CriteriaBuilderTestPerson())
+        ((RxQuery) query).singleResult() >> observable
+
+        when:
+        def result = criteria.find { eq('name', 'a') }
+
+        then:
+        result.is(observable)
+        1 * query.add(_)
+    }
+
+    void "find() with no closure delegates to get() with a null closure"() {
+        given:
+        def criteria = newBuilder()
+        query.singleResult() >> Observable.empty()
+
+        when:
+        def result = criteria.find()
+
+        then:
+        result != null
+        criteria.uniqueResult
+    }
+
+    void "findAll(Closure) delegates to findAll with an empty argument map"() {
+        given:
+        def criteria = newBuilder()
+        Observable<CriteriaBuilderTestPerson> observable = Observable.just(new CriteriaBuilderTestPerson())
+        ((RxQuery) query).findAll([:]) >> observable
+
+        when:
+        def result = criteria.findAll { eq('name', 'a') }
+
+        then:
+        result.is(observable)
+        1 * query.add(_)
+    }
+
+    void "findAll(Map, Closure) prepares the query and returns the observable results"() {
+        given:
+        def criteria = newBuilder()
+        Observable<CriteriaBuilderTestPerson> observable = Observable.just(new CriteriaBuilderTestPerson())
+        ((RxQuery) query).findAll([:]) >> observable
+
+        when:
+        def result = criteria.findAll([:]) { eq('name', 'a') }
+
+        then:
+        result.is(observable)
+        1 * query.add(_)
+    }
+
+    void "findAll applies any pre-populated order entries before executing"() {
+        given:
+        def criteria = newBuilder()
+        criteria.orderEntries << Query.Order.asc('name')
+        ((RxQuery) query).findAll([:]) >> Observable.empty()
+
+        when:
+        criteria.findAll()
+
+        then:
+        1 * query.order(_)
+    }
+
+    void "list(Map, Closure) collects findAll's results into a single observable list"() {
+        given:
+        def criteria = newBuilder()
+        ((RxQuery) query).findAll([:]) >> Observable.from(['a', 'b'])
+
+        when:
+        Observable<List> result = criteria.list([:]) { eq('name', 'a') }
+
+        then:
+        result.toBlocking().first() == ['a', 'b']
+    }
+
+    void "list(Closure) collects findAll's results into a single observable list"() {
+        given:
+        def criteria = newBuilder()
+        ((RxQuery) query).findAll([:]) >> Observable.from(['a', 'b'])
+
+        when:
+        Observable<List> result = criteria.list { eq('name', 'a') }
+
+        then:
+        result.toBlocking().first() == ['a', 'b']
+    }
+
+    void "count(Map, Closure) applies a count projection and returns a single observable result"() {
+        given:
+        def criteria = newBuilder()
+        Query.ProjectionList projectionList = Mock(Query.ProjectionList)
+        query.projections() >> projectionList
+        ((RxQuery) query).singleResult([:]) >> Observable.just(5)
+
+        when:
+        Observable<Number> result = criteria.count([:]) { eq('name', 'a') }
+
+        then:
+        result.toBlocking().first() == 5
+        1 * projectionList.count()
+    }
+
+    void "count(Closure) delegates to count with an empty argument map"() {
+        given:
+        def criteria = newBuilder()
+        Query.ProjectionList projectionList = Mock(Query.ProjectionList)
+        query.projections() >> projectionList
+        ((RxQuery) query).singleResult([:]) >> Observable.just(5)
+
+        when:
+        Observable<Number> result = criteria.count { eq('name', 'a') }
+
+        then:
+        result.toBlocking().first() == 5
+    }
+
+    void "listDistinct(Map, Closure) applies a distinct projection and collects the results"() {
+        given:
+        def criteria = newBuilder()
+        Query.ProjectionList projectionList = Mock(Query.ProjectionList)
+        query.projections() >> projectionList
+        ((RxQuery) query).findAll([:]) >> Observable.from(['a'])
+
+        when:
+        Observable<List> result = criteria.listDistinct([:]) { eq('name', 'a') }
+
+        then:
+        result.toBlocking().first() == ['a']
+        1 * projectionList.distinct()
+    }
+
+    void "listDistinct(Closure) delegates to listDistinct with an empty argument map"() {
+        given:
+        def criteria = newBuilder()
+        Query.ProjectionList projectionList = Mock(Query.ProjectionList)
+        query.projections() >> projectionList
+        ((RxQuery) query).findAll([:]) >> Observable.from(['a'])
+
+        when:
+        Observable<List> result = criteria.listDistinct { eq('name', 'a') }
+
+        then:
+        result.toBlocking().first() == ['a']
+    }
+
+    void "a call-style invocation resolves through the reactive invokeList override"() {
+        given:
+        def criteria = newBuilder()
+        ((RxQuery) query).findAll() >> Observable.from(['a'])
+
+        when:
+        def result = criteria.call { eq('name', 'a') }
+
+        then:
+        result.toBlocking().first() == 'a'
+    }
+}
+
+class CriteriaBuilderTestPerson {
+    Long id
+    String name
+}


(grails-core) 02/02: Fix IntelliJ inspection warnings on AbstractCriteriaBuilder

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

borinquenkid pushed a commit to branch chore/cleanup-AbstractCriteriaBuilder
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 377a7f50ef183a9cc4772570ea2a665433067dd1
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Wed Aug 12 13:35:26 2026 -0500

    Fix IntelliJ inspection warnings on AbstractCriteriaBuilder
    
    - Removed 2 truly-unused public constants (ORDER_DESCENDING/ORDER_ASCENDING)
      after confirming zero references anywhere in the repo; the class's own
      order(String, String) already used grails.gorm.CriteriaBuilder's identical
      constants instead.
    - Parameterized raw uses of Class, PersistentProperty, Association, Closure,
      QueryableCriteria, and DetachedCriteria.
    - Converted 3 instanceof-plus-cast blocks to Java pattern variables.
    - Replaced a redundant `associationQuery instanceof AssociationQuery` check
      with a null check, since Query.createQuery(String) is declared to return
      AssociationQuery directly, making the instanceof always true.
    - Replaced manual list.get(size()-1)/remove(size()-1) with getLast()/removeLast().
    - Extracted the duplicated MetaMethod lookup-and-invoke block in invokeMethod
      into a private helper, using a NOT_FOUND sentinel to preserve the exact
      "found but returned null" vs "not found" distinction the original two
      copies each handled inline.
    - Left 2 items as accepted/suppressed rather than "fixed" incorrectly:
      in(String, Collection) and inList(String, Collection) keep a raw Collection
      parameter with @SuppressWarnings("rawtypes"), since Criteria's own
      declaration is raw and a parameterized override would not actually
      override it (name clash, not an override, due to erasure); addToCriteria
      keeps @SuppressWarnings("UnusedReturnValue") since most callers discard its
      return value by design, even though a few legitimately use it.
    - Also drops an unused Criteria import from grails.gorm.rx.CriteriaBuilder.
    
    AbstractCriteriaBuilder coverage held at 99.7% lines / 100% methods / 90%
    branches throughout; all changes verified semantic-preserving.
    
    Co-Authored-By: Claude Sonnet 5 <no...@anthropic.com>
---
 .../query/criteria/AbstractCriteriaBuilder.java    | 103 +++++++++++----------
 .../groovy/grails/gorm/rx/CriteriaBuilder.groovy   |   1 -
 2 files changed, 54 insertions(+), 50 deletions(-)

diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java
index 5e5ca3b1db..69cbbc12ad 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java
@@ -40,7 +40,6 @@ import org.grails.datastore.mapping.model.MappingContext;
 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.query.AssociationQuery;
 import org.grails.datastore.mapping.query.Query;
 import org.grails.datastore.mapping.query.QueryCreator;
 import org.grails.datastore.mapping.query.Restrictions;
@@ -55,14 +54,14 @@ import org.grails.datastore.mapping.query.api.QueryableCriteria;
  * @since 6.0
  */
 public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implements Criteria, ProjectionList {
-    public static final String ORDER_DESCENDING = "desc";
-    public static final String ORDER_ASCENDING = "asc";
+    // sentinel distinguishing "no matching MetaMethod" from "matched and returned null"
+    private static final Object NOT_FOUND = new Object();
 
     protected static final String ROOT_CALL = "call";
     protected static final String ROOT_DO_CALL = "doCall";
     protected static final String SCROLL_CALL = "scroll";
 
-    protected final Class targetClass;
+    protected final Class<?> targetClass;
     protected final QueryCreator queryCreator;
     protected Query query;
     protected boolean uniqueResult = false;
@@ -74,7 +73,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
     protected boolean readOnly;
     private List<Query.Junction> logicalExpressionStack = new ArrayList<>();
 
-    public AbstractCriteriaBuilder(final Class targetClass, QueryCreator queryCreator, final MappingContext mappingContext) {
+    public AbstractCriteriaBuilder(final Class<?> targetClass, QueryCreator queryCreator, final MappingContext mappingContext) {
         Assert.notNull(targetClass, "Argument [targetClass] cannot be null");
         Assert.notNull(mappingContext, "Argument [session] cannot be null");
 
@@ -89,7 +88,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
         this.queryCreator = queryCreator;
     }
 
-    public Class getTargetClass() {
+    public Class<?> getTargetClass() {
         return this.targetClass;
     }
 
@@ -296,21 +295,20 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
             return result;
         }
 
-        MetaMethod metaMethod = getMetaClass().getMetaMethod(name, args);
-        if (metaMethod != null) {
-            return metaMethod.invoke(this, args);
+        Object result = invokeMetaMethod(getMetaClass(), this, name, args);
+        if (result != NOT_FOUND) {
+            return result;
         }
 
-        metaMethod = queryMetaClass.getMetaMethod(name, args);
-        if (metaMethod != null) {
-            return metaMethod.invoke(query, args);
+        result = invokeMetaMethod(queryMetaClass, query, name, args);
+        if (result != NOT_FOUND) {
+            return result;
         }
 
         if (args.length == 1 && args[0] instanceof Closure) {
 
-            final PersistentProperty property = persistentEntity.getPropertyByName(name);
-            if (property instanceof Association) {
-                Association association = (Association) property;
+            final PersistentProperty<?> property = persistentEntity.getPropertyByName(name);
+            if (property instanceof Association<?> association) {
                 Query previousQuery = query;
                 PersistentEntity previousEntity = persistentEntity;
                 List<Query.Junction> previousLogicalExpressionStack = logicalExpressionStack;
@@ -318,7 +316,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
                 Query associationQuery;
                 try {
                     associationQuery = query.createQuery(property.getName());
-                    if (associationQuery instanceof AssociationQuery) {
+                    if (associationQuery != null) {
                         addToCriteria((Query.Criterion) associationQuery);
                     }
                     query = associationQuery;
@@ -339,6 +337,11 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
         throw new MissingMethodException(name, getClass(), args);
     }
 
+    private Object invokeMetaMethod(MetaObjectProtocol metaClass, Object target, String name, Object[] args) {
+        MetaMethod metaMethod = metaClass.getMetaMethod(name, args);
+        return metaMethod != null ? metaMethod.invoke(target, args) : NOT_FOUND;
+    }
+
     protected Object invokeList() {
         Object result;
         result = query.list();
@@ -351,23 +354,23 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @param callable The closure defining the projections
      * @return The projections list
      */
-    public ProjectionList projections(Closure callable) {
+    public ProjectionList projections(Closure<?> callable) {
         projectionList = query.projections();
         invokeClosureNode(callable);
         return projectionList;
     }
 
-    public Criteria and(Closure callable) {
+    public Criteria and(Closure<?> callable) {
         handleJunction(new Query.Conjunction(), callable);
         return this;
     }
 
-    public Criteria or(Closure callable) {
+    public Criteria or(Closure<?> callable) {
         handleJunction(new Query.Disjunction(), callable);
         return this;
     }
 
-    public Criteria not(Closure callable) {
+    public Criteria not(Closure<?> callable) {
         handleJunction(new Query.Negation(), callable);
         return this;
     }
@@ -460,13 +463,12 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
-    public Criteria eqAll(String propertyName, Closure propertyValue) {
+    public Criteria eqAll(String propertyName, Closure<?> propertyValue) {
         return eqAll(propertyName, buildQueryableCriteria(propertyValue));
     }
 
-    @SuppressWarnings("unchecked")
-    private QueryableCriteria buildQueryableCriteria(Closure queryClosure) {
-        return new DetachedCriteria(targetClass).build(queryClosure);
+    private QueryableCriteria<?> buildQueryableCriteria(Closure<?> queryClosure) {
+        return new DetachedCriteria<>(targetClass).build(queryClosure);
     }
 
     /**
@@ -477,7 +479,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
-    public Criteria gtAll(String propertyName, Closure propertyValue) {
+    public Criteria gtAll(String propertyName, Closure<?> propertyValue) {
         return gtAll(propertyName, buildQueryableCriteria(propertyValue));
     }
 
@@ -489,7 +491,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
-    public Criteria ltAll(String propertyName, Closure propertyValue) {
+    public Criteria ltAll(String propertyName, Closure<?> propertyValue) {
         return ltAll(propertyName, buildQueryableCriteria(propertyValue));
     }
 
@@ -501,7 +503,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
-    public Criteria geAll(String propertyName, Closure propertyValue) {
+    public Criteria geAll(String propertyName, Closure<?> propertyValue) {
         return geAll(propertyName, buildQueryableCriteria(propertyValue));
     }
 
@@ -513,7 +515,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
-    public Criteria leAll(String propertyName, Closure propertyValue) {
+    public Criteria leAll(String propertyName, Closure<?> propertyValue) {
         return leAll(propertyName, buildQueryableCriteria(propertyValue));
     }
 
@@ -525,7 +527,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
-    public Criteria eqAll(String propertyName, QueryableCriteria propertyValue) {
+    public Criteria eqAll(String propertyName, QueryableCriteria<?> propertyValue) {
         validatePropertyName(propertyName, "eqAll");
         addToCriteria(new Query.EqualsAll(propertyName, propertyValue));
         return this;
@@ -539,14 +541,14 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
-    public Criteria gtAll(String propertyName, QueryableCriteria propertyValue) {
+    public Criteria gtAll(String propertyName, QueryableCriteria<?> propertyValue) {
         validatePropertyName(propertyName, "gtAll");
         addToCriteria(new Query.GreaterThanAll(propertyName, propertyValue));
         return this;
     }
 
     @Override
-    public Criteria gtSome(String propertyName, QueryableCriteria propertyValue) {
+    public Criteria gtSome(String propertyName, QueryableCriteria<?> propertyValue) {
         validatePropertyName(propertyName, "gtSome");
         addToCriteria(new Query.GreaterThanSome(propertyName, propertyValue));
         return this;
@@ -558,7 +560,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
     }
 
     @Override
-    public Criteria geSome(String propertyName, QueryableCriteria propertyValue) {
+    public Criteria geSome(String propertyName, QueryableCriteria<?> propertyValue) {
         validatePropertyName(propertyName, "geSome");
         addToCriteria(new Query.GreaterThanEqualsSome(propertyName, propertyValue));
         return this;
@@ -570,7 +572,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
     }
 
     @Override
-    public Criteria ltSome(String propertyName, QueryableCriteria propertyValue) {
+    public Criteria ltSome(String propertyName, QueryableCriteria<?> propertyValue) {
         validatePropertyName(propertyName, "ltSome");
         addToCriteria(new Query.LessThanEqualsSome(propertyName, propertyValue));
         return this;
@@ -582,7 +584,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
     }
 
     @Override
-    public Criteria leSome(String propertyName, QueryableCriteria propertyValue) {
+    public Criteria leSome(String propertyName, QueryableCriteria<?> propertyValue) {
         validatePropertyName(propertyName, "leSome");
         addToCriteria(new Query.LessThanEqualsSome(propertyName, propertyValue));
         return this;
@@ -635,7 +637,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
-    public Criteria ltAll(String propertyName, QueryableCriteria propertyValue) {
+    public Criteria ltAll(String propertyName, QueryableCriteria<?> propertyValue) {
         validatePropertyName(propertyName, "ltAll");
         addToCriteria(new Query.LessThanAll(propertyName, propertyValue));
         return this;
@@ -649,7 +651,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
-    public Criteria geAll(String propertyName, QueryableCriteria propertyValue) {
+    public Criteria geAll(String propertyName, QueryableCriteria<?> propertyValue) {
         validatePropertyName(propertyName, "geAll");
         addToCriteria(new Query.GreaterThanEqualsAll(propertyName, propertyValue));
         return this;
@@ -663,7 +665,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
-    public Criteria leAll(String propertyName, QueryableCriteria propertyValue) {
+    public Criteria leAll(String propertyName, QueryableCriteria<?> propertyValue) {
         validatePropertyName(propertyName, "leAll");
         addToCriteria(new Query.LessThanEqualsAll(propertyName, propertyValue));
         return this;
@@ -845,6 +847,9 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
+    // raw Collection matches Criteria.in's own raw declaration; Collection<?> here would not
+    // override it (same erasure, but treated as a name clash rather than an override)
+    @SuppressWarnings("rawtypes")
     @Override
     public Criteria in(String propertyName, Collection values) {
         validatePropertyName(propertyName, "in");
@@ -861,6 +866,8 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      *
      * @return A Criterion instance
      */
+    // raw Collection matches Criteria.inList's own raw declaration; see in(String, Collection)
+    @SuppressWarnings("rawtypes")
     @Override
     public Criteria inList(String propertyName, Collection values) {
         in(propertyName, values);
@@ -1096,7 +1103,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
                     "] restriction with null property name");
         }
 
-        PersistentProperty property = persistentEntity.getPropertyByName(propertyName);
+        PersistentProperty<?> property = persistentEntity.getPropertyByName(propertyName);
         if (property == null && persistentEntity.getIdentity().getName().equals(propertyName)) {
             property = persistentEntity.getIdentity();
         }
@@ -1122,22 +1129,21 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
     }
 
     protected void invokeClosureNode(Object args) {
-        if (args instanceof Closure) {
-            Closure callable = (Closure) args;
+        if (args instanceof Closure<?> callable) {
             callable.setDelegate(this);
             callable.setResolveStrategy(Closure.DELEGATE_FIRST);
             callable.call();
         }
     }
 
-    private void handleJunction(Query.Junction junction, Closure callable) {
+    private void handleJunction(Query.Junction junction, Closure<?> callable) {
         logicalExpressionStack.add(junction);
         try {
             if (callable != null) {
                 invokeClosureNode(callable);
             }
         } finally {
-            Query.Junction logicalExpression = logicalExpressionStack.remove(logicalExpressionStack.size() - 1);
+            Query.Junction logicalExpression = logicalExpressionStack.removeLast();
             addToCriteria(logicalExpression);
         }
     }
@@ -1147,17 +1153,16 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
         * this might be either the root criteria or a currently open
         * LogicalExpression.
         */
+    @SuppressWarnings("UnusedReturnValue")
     protected Query.Criterion addToCriteria(Query.Criterion c) {
-        if (c instanceof Query.PropertyCriterion) {
-            Query.PropertyCriterion pc = (Query.PropertyCriterion) c;
-
+        if (c instanceof Query.PropertyCriterion pc) {
             Object value = pc.getValue();
-            if (value instanceof Closure) {
-                pc.setValue(buildQueryableCriteria((Closure) value));
+            if (value instanceof Closure<?> closureValue) {
+                pc.setValue(buildQueryableCriteria(closureValue));
             }
         }
         if (!logicalExpressionStack.isEmpty()) {
-            logicalExpressionStack.get(logicalExpressionStack.size() - 1).add(c);
+            logicalExpressionStack.getLast().add(c);
         }
         else {
             if (query == null) {
@@ -1172,7 +1177,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
         return query;
     }
 
-    public void build(Closure criteria) {
+    public void build(Closure<?> criteria) {
         if (criteria != null) {
             invokeClosureNode(criteria);
         }
diff --git a/grails-datamapping-rx/src/main/groovy/grails/gorm/rx/CriteriaBuilder.groovy b/grails-datamapping-rx/src/main/groovy/grails/gorm/rx/CriteriaBuilder.groovy
index 3baa9342ca..fb7c6b78ef 100644
--- a/grails-datamapping-rx/src/main/groovy/grails/gorm/rx/CriteriaBuilder.groovy
+++ b/grails-datamapping-rx/src/main/groovy/grails/gorm/rx/CriteriaBuilder.groovy
@@ -23,7 +23,6 @@ import org.grails.datastore.gorm.query.criteria.AbstractCriteriaBuilder
 import org.grails.datastore.mapping.model.MappingContext
 import org.grails.datastore.mapping.query.Query
 import org.grails.datastore.mapping.query.QueryCreator
-import org.grails.datastore.mapping.query.api.Criteria
 import org.grails.datastore.rx.query.RxQuery
 import rx.Observable