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 <[email protected]>
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 <[email protected]>
---
 .../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
+}

Reply via email to