jdaugherty commented on code in PR #16140:
URL: https://github.com/apache/grails-core/pull/16140#discussion_r4146441751


##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java:
##########
@@ -58,11 +56,14 @@ public abstract class AbstractCriteriaBuilder extends 
GroovyObjectSupport implem
     public static final String ORDER_DESCENDING = "desc";

Review Comment:
   Restored these. They're public on an abstract class that 
`grails.gorm.rx.CriteriaBuilder` inherits, so dropping them is an API break for 
a minor release even though nothing in-repo references them. `order(String, 
String)` and `isCriteriaConstructionMethod` now use the class's own constants 
instead of reaching into the `grails.gorm.CriteriaBuilder` subclass, which also 
removes that import.



##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java:
##########
@@ -810,9 +858,10 @@ public Criteria ilike(String propertyName, Object 
propertyValue) {
      *
      * @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");
+        validatePropertyName(propertyName, "rlike");

Review Comment:
   `rlike` validated and reported its property under the `like` label; both now 
say `rlike`.



##########
grails-datamapping-core/src/test/groovy/grails/gorm/CriteriaBuilderSpec.groovy:
##########
@@ -18,117 +18,1176 @@
  */
 package grails.gorm
 
-import grails.gorm.annotation.Entity
-import org.grails.datastore.mapping.simple.SimpleMapDatastore
-import spock.lang.AutoCleanup
+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
 
 /**
- * {@code CriteriaBuilder} is the concrete class real callers get from
- * {@code GormStaticApi#createCriteria()} (see {@code GormStaticApi.groovy}'s
- * {@code new CriteriaBuilder(persistentClass, session)}) - and both it and 
its abstract superclass
- * {@code AbstractCriteriaBuilder} had 0% coverage from this module's own 
JaCoCo perspective (real
- * usage is exercised only via adapter-specific subclasses/specs in other 
modules, which don't count
- * toward this module's report - see item 14's note on cross-module coverage 
attribution).
- *
- * This PR's diff added {@code ensureQueryIsInitialized()} guards to several
- * {@code AbstractCriteriaBuilder} methods (`cache`, `join`, `select`, 
`order`, `invokeList`,
- * `projections`) plus a new `getPersistentEntity()` getter - fixing the exact 
NPE
- * item 9 found and left as a known gap: {@code query} is null on a criteria 
builder obtained
- * directly from `createCriteria()` until something inside a 
`.list{}`/`.get{}` closure first
- * touches it.
+ * 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 {
 
-    @AutoCleanup
-    SimpleMapDatastore datastore = new 
SimpleMapDatastore(CriteriaBuilderSpecBook, CriteriaBuilderSpecAuthor)
+    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
+    }
+    Session session = Stub(Session) {
+        getMappingContext() >> mappingContext
+    }
+
+    CriteriaBuilder<CriteriaBuilderTestPerson> newBuilder() {
+        new 
CriteriaBuilder<CriteriaBuilderTestPerson>(CriteriaBuilderTestPerson, session, 
query)
+    }
+
+    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)
 
-    void "cache/select/order can be called directly on a bare createCriteria() 
without a wrapping closure"() {
-        given: "a criteria builder obtained directly, not via .list{}/.get{} - 
previously NPE'd on a null query"
-        def criteria = CriteriaBuilderSpecBook.createCriteria()
+        then:
+        IllegalArgumentException e = thrown()
+        e.message.contains(CriteriaBuilderTestPerson.name)
+    }
 
+    void "getTargetClass returns the class the criteria was built for"() {
         expect:
-        criteria.cache(true).is(criteria)
-        criteria.select('title').is(criteria)
-        criteria.order('title').is(criteria)
-        criteria.order('title', 'desc').is(criteria)
-        criteria.getPersistentEntity() != null
+        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 "join(String) can be called directly on a bare createCriteria() 
without a wrapping closure"() {
-        given: "join(String) had the same NPE as cache/select before this fix 
- test with a real association"
-        def criteria = CriteriaBuilderSpecAuthor.createCriteria()
+    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:
-        criteria.join('books').is(criteria)
+        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) adds a #criterion.simpleName criterion 
for the property and value"() {
+        given:
+        def criteria = newBuilder()
+
+        when:
+        def result = criteria."$method"('name', 'value')
+
+        then:
+        1 * query.add({ it.getClass() == criterion && it.property == 'name' && 
it.value == 'value' })

Review Comment:
   Tightened every criterion interaction from `1 * query.add(_)` to check the 
concrete `Query.Criterion` class plus property/value (or `otherProperty`, 
`from`/`to`, subquery identity, junction contents). A wildcard only proves 
*something* was added, and it's what hid the `ltSome` mismatch.



##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java:
##########
@@ -562,9 +599,9 @@ public Criteria geSome(String propertyName, Closure<?> 
propertyValue) {
     }
 
     @Override
-    public Criteria ltSome(String propertyName, QueryableCriteria 
propertyValue) {
+    public Criteria ltSome(String propertyName, QueryableCriteria<?> 
propertyValue) {
         validatePropertyName(propertyName, "ltSome");
-        addToCriteria(new Query.LessThanEqualsSome(propertyName, 
propertyValue));
+        addToCriteria(new Query.LessThanSome(propertyName, propertyValue));

Review Comment:
   Pre-existing bug that the wildcard `1 * query.add(_)` interactions in the 
spec let through: `ltSome` built a `LessThanEqualsSome`. 
`AbstractDetachedCriteria.ltSome` already uses `LessThanSome`, and the 
Hibernate 5/7 adapters and `JpaQueryBuilder` all handle it, so this is purely a 
wrong-criterion fix. Reverting it fails the spec.



##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java:
##########
@@ -1113,16 +1184,14 @@ private void handleJunction(Query.Junction junction, 
Closure callable) {
         * LogicalExpression.
         */
     protected Query.Criterion addToCriteria(Query.Criterion c) {

Review Comment:
   Dropped `@SuppressWarnings("UnusedReturnValue")`: it's an IntelliJ 
inspection key that javac, PMD and Checkstyle don't recognize, so it only adds 
noise.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to