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