You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2025/08/29 02:50:04 UTC

(grails-core) 01/04: Better testing coverage for HibernateCriteriaBuilder

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

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

commit 9c75b40c300a3482a09bf07361bebe6a18d79429
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Thu Aug 28 19:53:42 2025 -0500

    Better testing coverage for HibernateCriteriaBuilder
---
 .../grails/orm/HibernateCriteriaBuilder.java       |  79 +++----
 .../GlobalConstraintWithCompositeIdSpec.groovy     |   2 +-
 .../hibernate/HibernateCriteriaBuilderSpec.groovy  | 251 +++++++++++++++++++++
 .../HibernateGormStaticApiSpecification.groovy     | 235 -------------------
 4 files changed, 278 insertions(+), 289 deletions(-)

diff --git a/grails-data-hibernate6/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java b/grails-data-hibernate6/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
index ac8a7da24c..9ade10998a 100644
--- a/grails-data-hibernate6/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
+++ b/grails-data-hibernate6/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
@@ -20,8 +20,6 @@ import grails.gorm.MultiTenant;
 import groovy.lang.Closure;
 import groovy.lang.DelegatesTo;
 import groovy.lang.GroovyObjectSupport;
-import groovy.lang.GroovySystem;
-import groovy.lang.MetaClass;
 import groovy.lang.MetaMethod;
 import groovy.lang.MissingMethodException;
 import groovy.util.logging.Slf4j;
@@ -38,11 +36,9 @@ import org.grails.datastore.mapping.query.Query;
 import org.grails.datastore.mapping.query.api.BuildableCriteria;
 import org.grails.datastore.mapping.query.api.Criteria;
 import org.grails.datastore.mapping.query.api.QueryableCriteria;
-import org.grails.datastore.mapping.reflect.NameUtils;
 import org.grails.orm.hibernate.AbstractHibernateDatastore;
 import org.grails.orm.hibernate.AbstractHibernateSession;
 import org.grails.orm.hibernate.GrailsHibernateTemplate;
-import org.grails.orm.hibernate.HibernateDatastore;
 import org.grails.orm.hibernate.query.HibernateQuery;
 import org.grails.orm.hibernate.query.HibernateQueryConstants;
 import org.hibernate.FetchMode;
@@ -191,12 +187,8 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
     protected SessionFactory sessionFactory;
     protected Session hibernateSession;
     protected Class<?> targetClass;
-    protected Query.Junction junction = new Query.Conjunction();
     protected CriteriaQuery criteriaQuery;
-    protected MetaClass criteriaMetaClass;
     protected boolean uniqueResult = false;
-    protected List<LogicalExpression> logicalExpressionStack = new ArrayList<LogicalExpression>();
-    protected List<String> associationStack = new ArrayList<String>();
     protected boolean participate;
     protected boolean scroll;
     protected boolean count;
@@ -250,6 +242,11 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
         return this;
     }
 
+    public Query.ProjectionList projections() {
+       return  hibernateQuery.projections();
+    }
+
+
     /**
      * A projection that selects a distince property name
      * @param propertyName The property name
@@ -383,7 +380,7 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
     }
 
     public ProjectionList count() {
-        return rowCount();
+        return hibernateQuery.projections().count();
     }
 
     /**
@@ -1170,11 +1167,21 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
 
     @Override
     public Object list(@DelegatesTo(Criteria.class) Closure c) {
+        hibernateQuery.setDetachedCriteria(new DetachedCriteria(targetClass));
         return invokeMethod(LIST_CALL, new Object[]{c});
     }
 
+    public List list() {
+        return hibernateQuery.list();
+    }
+
+    public Object singleResult() {
+        return hibernateQuery.singleResult();
+    }
+
     @Override
     public Object list(Map params, @DelegatesTo(Criteria.class) Closure c) {
+        hibernateQuery.setDetachedCriteria(new DetachedCriteria(targetClass));
         return invokeMethod(LIST_CALL, new Object[]{params, c});
     }
     
@@ -1204,6 +1211,7 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
     @SuppressWarnings("rawtypes")
     @Override
     public Object invokeMethod(String name, Object obj) {
+
         Object[] args = obj.getClass().isArray() ? (Object[])obj : new Object[]{obj};
 
         if (paginationEnabledList && SET_RESULT_TRANSFORMER_CALL.equals(name) && args.length == 1 &&
@@ -1230,6 +1238,14 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
             // Check for pagination params
             if (name.equals(LIST_CALL) && args.length == 2) {
                 paginationEnabledList = true;
+                if (args[0] instanceof Map map ) {
+                    if (map.get("max") instanceof Number max) {
+                        hibernateQuery.maxResults(max.intValue());
+                    }
+                    if (map.get("offset") instanceof Number offset) {
+                        hibernateQuery.firstResult(offset.intValue());
+                    }
+                }
                 invokeClosureNode(args[1]);
             }
             else {
@@ -1281,22 +1297,13 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
             return result;
         }
 
-        if (criteriaQuery == null) createCriteriaInstance();
 
         MetaMethod metaMethod = getMetaClass().getMetaMethod(name, args);
         if (metaMethod != null) {
             return metaMethod.invoke(this, args);
         }
 
-        MetaClass metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(targetClass);
-        metaMethod = metaClass.getMetaMethod(name, args);
-        if (metaMethod != null) {
-            return metaMethod.invoke(criteriaQuery, args);
-        }
-        metaMethod = metaClass.getMetaMethod(NameUtils.getSetterName(name), args);
-        if (metaMethod != null) {
-            return metaMethod.invoke(criteriaQuery, args);
-        }
+
 
         if (isAssociationQueryMethod(args) || isAssociationQueryWithJoinSpecificationMethod(args)) {
             final boolean hasMoreThanOneArg = args.length > 1;
@@ -1376,10 +1383,6 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
         throw new MissingMethodException(name, getClass(), args);
     }
 
-    protected List createPagedResultList(Map args) {
-        GrailsHibernateTemplate ght = new GrailsHibernateTemplate(sessionFactory, (HibernateDatastore) datastore, getDefaultFlushMode());
-        return null;
-    }
 
     private boolean isAssociationQueryMethod(Object[] args) {
         return args.length == 1 && args[0] instanceof Closure;
@@ -1390,16 +1393,6 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
     }
 
 
-    private String getAssociationPath() {
-        StringBuilder fullPath = new StringBuilder();
-        for (Object anAssociationStack : associationStack) {
-            String propertyName = (String) anAssociationStack;
-            if (fullPath.length() > 0) fullPath.append(".");
-            fullPath.append(propertyName);
-        }
-        return fullPath.toString();
-    }
-
     private boolean isCriteriaConstructionMethod(String name, Object[] args) {
         return (name.equals(LIST_CALL) && args.length == 2 && args[0] instanceof Map && args[1] instanceof Closure) ||
                 (name.equals(ROOT_CALL) ||
@@ -1411,13 +1404,7 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
                         name.equals(SCROLL_CALL) && args.length == 1 && args[0] instanceof Closure);
     }
 
-    protected void createCriteriaInstance() {
-        // no-op
-    }
 
-    protected void cacheCriteriaMapping() {
-        // no-op
-    }
 
     private void invokeClosureNode(Object args) {
         Closure<?> callable = (Closure<?>)args;
@@ -1426,20 +1413,6 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
         callable.call();
     }
 
-    /**
-     * adds and returns the given criterion to the currently active criteria set.
-     * this might be either the root criteria or a currently open
-     * LogicalExpression.
-     */
-    protected Query.Criterion addToCriteria(Query.Criterion c) {
-        if (false) {
-            // logicalExpressionStack.get(logicalExpressionStack.size() - 1).args.add(c);
-        }
-        else {
-            junction.add(c);
-        }
-        return c;
-    }
 
     /**
      * Returns the criteria instance
diff --git a/grails-data-hibernate6/core/src/test/groovy/grails/gorm/specs/compositeid/GlobalConstraintWithCompositeIdSpec.groovy b/grails-data-hibernate6/core/src/test/groovy/grails/gorm/specs/compositeid/GlobalConstraintWithCompositeIdSpec.groovy
index c946b43535..387bf4d024 100644
--- a/grails-data-hibernate6/core/src/test/groovy/grails/gorm/specs/compositeid/GlobalConstraintWithCompositeIdSpec.groovy
+++ b/grails-data-hibernate6/core/src/test/groovy/grails/gorm/specs/compositeid/GlobalConstraintWithCompositeIdSpec.groovy
@@ -36,7 +36,7 @@ class GlobalConstraintWithCompositeIdSpec extends HibernateGormDatastoreSpec {
     def setupSpec() {
         manager.addAllDomainClasses([ParentB, ChildB, DomainB])
         manager.grailsConfig = [
-                'dataSource.url'                 : "jdbc:tc:postgresql:latest:///dev_db",
+                'dataSource.url'               : "jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000",
                 'dataSource.dbCreate'            : 'create-drop',
                 'dataSource.formatSql'           : 'true',
                 'dataSource.logSql'              : 'true',
diff --git a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateCriteriaBuilderSpec.groovy b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateCriteriaBuilderSpec.groovy
new file mode 100644
index 0000000000..77e43baef9
--- /dev/null
+++ b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateCriteriaBuilderSpec.groovy
@@ -0,0 +1,251 @@
+package org.grails.orm.hibernate
+
+import grails.gorm.annotation.Entity
+import grails.gorm.specs.HibernateGormDatastoreSpec
+import grails.orm.HibernateCriteriaBuilder
+
+import java.math.RoundingMode
+
+class HibernateCriteriaBuilderSpec extends HibernateGormDatastoreSpec {
+
+    def setupSpec() {
+        manager.addAllDomainClasses([Account, Transaction])
+    }
+
+    HibernateCriteriaBuilder builder
+
+    def setup() {
+        def fred = new Account(balance: 250, firstName: "Fred", lastName: "Flintstone", branch: "Bedrock").save(failOnError: true)
+        def barney = new Account(balance: 500, firstName: "Barney", lastName: "Rubble", branch: "Bedrock").save(failOnError: true)
+        new Account(balance: 100, firstName: "Wilma", lastName: "Flintstone", branch: "Bedrock").save(failOnError: true)
+        new Account(balance: 1000, firstName: "Pebbles", lastName: "Flintstone", branch: "Slate Rock and Gravel").save(failOnError: true)
+        new Account(balance: 50, firstName: "Bam-Bam", lastName: "Rubble", branch: null).save(failOnError: true)
+
+        fred.addToTransactions(new Transaction(amount: 10))
+        fred.addToTransactions(new Transaction(amount: 20))
+        fred.save()
+
+        barney.addToTransactions(new Transaction(amount: 50))
+        barney.save(flush: true,failOnError: true)
+
+        builder = new HibernateCriteriaBuilder(Account, manager.hibernateDatastore.sessionFactory, manager.hibernateDatastore)
+    }
+
+    void "test get with eq criteria"() {
+        when:
+        def result = builder.get {
+            eq("firstName", "Fred")
+        }
+
+        then:
+        result.firstName == "Fred"
+    }
+
+    void "test idEq criteria"() {
+        given:
+        def fred = Account.findByFirstName("Fred")
+
+        when:
+        def result = builder.get {
+            idEq(fred.id)
+        }
+
+        then:
+        result.id == fred.id
+        result.firstName == "Fred"
+    }
+
+    void "test list with various criteria"() {
+        when:
+        def results = builder.list {
+            gt("balance", BigDecimal.valueOf(200))
+            or {
+                eq("lastName", "Flintstone")
+                like("branch", "Bedrock")
+            }
+            'in'("firstName", ["Fred", "Barney", "Pebbles"])
+        }
+
+        then:
+        results.size() == 3
+        results*.firstName.sort() == ["Barney", "Fred", "Pebbles"]
+    }
+
+    void "test ilike criteria"() {
+        when:
+        def results = builder.list {
+            ilike("firstName", "fr%")
+        }
+
+        then:
+        results.size() == 1
+        results[0].firstName == "Fred"
+    }
+
+    void "test isNull criteria"() {
+        when:
+        def results = builder.list {
+            isNull("branch")
+        }
+
+        then:
+        results.size() == 1
+        results[0].firstName == "Bam-Bam"
+    }
+
+    void "test count projection"() {
+        when:
+        def count = builder.get {
+            projections {
+                count()
+            }
+            eq("lastName", "Flintstone")
+        }
+
+        then:
+        count == 3
+    }
+
+    void "test sum and avg projection"() {
+        when:
+        def projections = builder.get {
+            projections {
+                sum('balance')
+                avg('balance')
+            }
+            eq("branch", "Bedrock")
+        }
+
+        then:
+        projections[0] == 850
+        new BigDecimal(projections[1]).setScale(2, RoundingMode.HALF_UP) == 283.33
+    }
+
+    void "test ordering and pagination"() {
+        when:
+        def results = builder.list(max: 2, offset: 1) {
+            order("firstName", "asc")
+        }
+
+        then:
+        results.size() == 2
+        results*.firstName == ["Barney", "Fred"]
+    }
+
+    void "test query on association"() {
+        when:
+        def results = builder.list {
+            transactions {
+                gt("amount", 40)
+            }
+        }
+
+        then:
+        results.size() == 1
+        results[0].firstName == "Barney"
+    }
+
+    void "test inequality and between criteria"() {
+        when:
+        def results = builder.list {
+            ne("firstName", "Fred")
+            ge("balance", BigDecimal.valueOf(60))
+            le("balance", BigDecimal.valueOf(1000))
+        }
+        then:
+        results*.firstName.toSet() == ["Barney","Wilma","Pebbles"] as Set
+    }
+
+    void "test isNotNull and size constraints on association"() {
+        when:
+        def results = builder.list {
+            isNotNull("branch")
+            sizeGe("transactions", 1)
+        }
+        then:
+        results*.firstName.toSet() == ["Fred","Barney"] as Set
+    }
+
+    void "test property to property comparisons and ordering desc"() {
+        when:
+        def results = builder.list {
+            geProperty("balance", "balance") // always true, validates path
+            order("balance", "desc")
+        }
+        then:
+        results[0].firstName == "Pebbles"
+        results[-1].firstName == "Bam-Bam"
+    }
+
+    void "test projections countDistinct groupProperty min max"() {
+        when:
+        def results = builder.list {
+            projections {
+                groupProperty("lastName")
+                countDistinct("firstName")
+                min("balance")
+                max("balance")
+            }
+        }
+        then:
+        results.size() >= 1
+    }
+
+    void "test inList array and collection variants and firstResult"() {
+        when:
+        def list1 = builder.list {
+            'in'("firstName", ["Fred", "Barney"] as Object[]) // array via 'in' alias
+        }
+        def list2 = builder.list {
+            inList("firstName", ["Fred", "Wilma"] as Object[]) // array variant
+        }
+        def paged = builder.list(max: 1) {
+            order("firstName", "asc")
+            firstResult(2)
+        }
+        then:
+        list1 instanceof List && list1.size() > 0
+        list2 instanceof List && list2.size() > 0
+        paged.size() == 1
+        paged[0].firstName == "Fred"
+    }
+    
+    void "test eq with ignoreCase param path and like/ilike methods"() {
+        when:
+        def results = builder.list {
+            eq("firstName", "Fr", [ignoreCase:true]) // should fallback to like with %Fr%
+        }
+        def likeRes = builder.list {
+            like("branch", "%Bedrock%")
+        }
+        def ilikeRes = builder.list {
+            ilike("branch", "%BEDROCK%")
+        }
+        then:
+        results instanceof List
+        likeRes.size() >= 1
+        ilikeRes.size() >= 1 
+    }
+}
+
+@Entity
+class Account {
+    String firstName
+    String lastName
+    BigDecimal balance
+    String branch
+    Set<Transaction> transactions
+
+    static hasMany = [transactions: Transaction]
+    static constraints = {
+        branch nullable:true
+    }
+}
+
+@Entity
+class Transaction {
+    BigDecimal amount
+    Date dateCreated
+
+    static belongsTo = [account: Account]
+}
diff --git a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpecification.groovy b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpecification.groovy
deleted file mode 100644
index e151ac3b20..0000000000
--- a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpecification.groovy
+++ /dev/null
@@ -1,235 +0,0 @@
-package org.grails.orm.hibernate
-
-import grails.gorm.specs.HibernateGormDatastoreSpec
-import grails.persistence.Entity
-import groovy.transform.EqualsAndHashCode
-import org.hibernate.ScrollableResults
-import org.hibernate.transform.ToListResultTransformer
-import spock.lang.Ignore
-
-
-class HibernateGormStaticApiTest extends HibernateGormDatastoreSpec{
-
-    def setupSpec() {
-        manager.addAllDomainClasses([Account, Transaction, Whatever])
-    }
-
-    Account fredAccount
-    Account barneyAccount
-    Account jamesAccount
-    Account maryAccount
-    def accountCriteria
-
-    def setup() {
-        fredAccount = new Account(balance: 250, firstName: "Fred", lastName: "Flintstone", branch: "Bedrock")
-        fredAccount.transactions << new Transaction(amount: 50, account: fredAccount)
-        fredAccount.save(flush:true)
-        barneyAccount = new Account(balance: 500, firstName: "Barney", lastName: "Rubble", branch: "Bedrock").save(flush: true)
-        jamesAccount = new Account(balance: 750, firstName: "James", lastName: "Bond", branch: "London").save(flush: true)
-        maryAccount = new Account(balance: 1000, firstName: "Mary", lastName: "Poppins", branch: "London").save(flush: true)
-        accountCriteria = Account.createCriteria()
-    }
-
-    def "equalsCriteria"() {
-        when:
-        def results   =  accountCriteria {
-                eq("branch","London")
-                maxResults(1)
-            }
-        then:
-            results.size() == 1
-            results[0] == jamesAccount
-    }
-
-    def "between"() {
-        when:
-        def results   =  accountCriteria {
-            between("balance", 500, 1000)
-            order("balance", "asc")
-        }
-        then:
-        results.size() == 3
-        results[0] == barneyAccount
-        results[1] == jamesAccount
-        results[2] == maryAccount
-    }
-
-    def "or"() {
-        when:
-        def results   =  accountCriteria {
-            or {
-                like("firstName", "Fred%")
-                like("firstName", "Barney%")
-
-            }
-            order("balance", "asc")
-        }
-        then:
-        results.size() == 2
-        results[0] == fredAccount
-        results[1] == barneyAccount
-    }
-
-    @Ignore("JPA Criteria requires a single predicate behind a not")
-    def "not"() {
-        when:
-        def results   =  accountCriteria {
-            not {
-                between("balance", 500, 1000)
-                eq("branch", "London")
-
-            }
-            order("balance", "asc")
-        }
-        then:
-        results.size() == 1
-        results[0] == fredAccount
-    }
-
-    def transactions() {
-        when:
-        def results = accountCriteria.list {
-            between("balance", 0, 500)
-            transactions {
-                between("amount", 50, 100)
-            }
-        }
-        then:
-            results.size() == 1
-            results[0] == fredAccount
-    }
-
-    def projectionsList() {
-        when:
-        def results = accountCriteria.get {
-            projections {
-                countDistinct('branch')
-                countDistinct('balance')
-            }
-        }
-        then:
-        results.size() == 2
-        results[0] == 2
-        results[1] == 4
-    }
-
-    def projections() {
-        when:
-        def results = accountCriteria.get {
-            projections {
-                countDistinct('branch')
-            }
-        }
-        then:
-        results == 2
-    }
-
-    def scroll() {
-        when:
-        ScrollableResults results = accountCriteria.scroll {
-            order("balance", "asc")
-        }
-        then:
-        results.scroll(1)
-        fredAccount == results.get()
-        results.last()
-        maryAccount == results.get()
-        results.previous()
-        jamesAccount == results.get()
-        results.first()
-        fredAccount == results.get()
-        results.next()
-        barneyAccount == results.get()
-    }
-
-    def "transformer"() {
-        when:
-        List results = accountCriteria.get {
-            resultTransformer(ToListResultTransformer.INSTANCE)
-            projections {
-                countDistinct('branch')
-                countDistinct('balance')
-            }
-        }
-        then:
-            results[0] == 2
-            results[1] == 4
-    }
-
-    def eagerFetching() {
-        when:
-        def results = accountCriteria.list {
-            eq("branch", "Bedrock")
-            join("transactions")
-        }
-        then:
-            results.size() == 1
-            results[0] == fredAccount
-            results[0].transactions.size() == 1
-            results[0].transactions[0].amount == 50
-    }
-
-    def listDistinct() {
-        new Account(balance: 250, firstName: "Fred", lastName: "Flintstone", branch: "Bedrock").save(flush:true)
-        when:
-        def results = accountCriteria.listDistinct {
-            property("branch")
-            order("branch")
-        }
-        then:
-        results.size() == 2
-        results[0] == "Bedrock"
-        results[1] == "London"
-    }
-
-//TODO: Chained Criteria
-    def multipleCriteria() {
-        given:
-        def accountCriteria = {
-            eq "branch", "Bedrock"
-        }
-        when:
-        def results = Account.withCriteria {
-            accountCriteria.delegate = accountCriteria
-            accountCriteria()
-            transactions {
-                between("amount", 50, 100)
-            }
-        }
-        then:
-        results.size() == 1
-        results[0] == fredAccount
-        results[0].transactions.size() == 1
-        results[0].transactions[0].amount == 50
-
-    }
-
-
-
-
-}
-
-@Entity
-@EqualsAndHashCode
-class Whatever {
-    Integer branch
-    Integer balance
-}
-
-@Entity
-class Transaction {
-    BigDecimal amount
-    static belongsTo = [account: Account]
-}
-
-@Entity
-@EqualsAndHashCode
-class Account {
-    BigDecimal balance
-    String branch
-    String firstName
-    String lastName
-    Set<Transaction> transactions = new HashSet<>()
-    static hasMany = [transactions:Transaction]
-
-}
\ No newline at end of file