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

jamesfredley pushed a commit to branch test/h7-functional-coverage
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 794ce6834abeffce927d42e0c32c6ebb3b6b544c
Author: James Fredley <[email protected]>
AuthorDate: Thu Jun 11 13:53:21 2026 -0400

    Match Hibernate 7 static finder parity
    
    Assisted-by: Hephaestus:openai/gpt-5.5
---
 .../orm/hibernate/HibernateGormStaticApi.groovy    | 29 ++++++--
 .../HibernateGormStaticApiFindWhereSpec.groovy     | 82 ++++++++++++++++++++++
 .../hibernate/HibernateGormStaticApiSpec.groovy    | 68 +++++++++++++++++-
 3 files changed, 173 insertions(+), 6 deletions(-)

diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy
index 4a47afc391..036e0ffeaf 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy
@@ -60,6 +60,7 @@ import org.grails.orm.hibernate.query.HibernateHqlQueryCreator
 import org.grails.orm.hibernate.query.HibernatePagedResultList
 import org.grails.orm.hibernate.query.MutationHqlQuery
 import org.grails.orm.hibernate.query.HibernateQuery
+import org.grails.orm.hibernate.query.HibernateQueryArgument
 import org.grails.orm.hibernate.query.HqlListQueryBuilder
 import org.grails.orm.hibernate.query.HqlQueryContext
 import org.grails.orm.hibernate.support.HibernateRuntimeUtils
@@ -353,7 +354,7 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
         if (!queryMap) return null
         Map coercedMap = queryMap.collectEntries { k, v -> [k.toString(), v] }
         String hql = buildWhereHql(coercedMap)
-        doSingleInternal(hql, coercedMap, [], args, false)
+        doSingleInternal(hql, buildWhereParams(coercedMap), [], 
buildFindWhereArgs(args), false)
     }
 
     @Override
@@ -361,14 +362,34 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
         if (!queryMap) return null
         Map coercedMap = queryMap.collectEntries { k, v -> [k.toString(), v] }
         String hql = buildWhereHql(coercedMap)
-        doListInternal(hql, coercedMap, [], args, false)
+        doListInternal(hql, buildWhereParams(coercedMap), [], args, false)
     }
 
     private String buildWhereHql(Map queryMap) {
-        String whereClause = queryMap.keySet().collect { Object key -> "$key = 
:$key" }.join(' and ')
+        String whereClause = queryMap.collect { Object key, Object value ->
+            String propertyName = validateWherePropertyName(key.toString())
+            value == null ? "$propertyName is null" : "$propertyName = 
:$propertyName"
+        }.join(' and ')
         return "from ${persistentEntity.name} where $whereClause"
     }
 
+    private String validateWherePropertyName(String propertyName) {
+        if (persistentEntity.getPropertyByName(propertyName) == null) {
+            throw new IllegalArgumentException("Property [$propertyName] is 
not a valid property of ${persistentEntity.name}")
+        }
+        return propertyName
+    }
+
+    private static Map buildWhereParams(Map queryMap) {
+        queryMap.findAll { Object key, Object value -> value != null }
+    }
+
+    private static Map buildFindWhereArgs(Map args) {
+        Map queryArgs = args ? new LinkedHashMap(args) : [:]
+        queryArgs[HibernateQueryArgument.MAX.value()] = 1
+        return queryArgs
+    }
+
     @Override
     List executeQuery(CharSequence query, Map namedParams, Map args) {
         doListInternal(query, namedParams, [], args, false)
@@ -392,7 +413,7 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
         List convertedIds = ids.collect { 
HibernateRuntimeUtils.convertValueToType(it, idType, conversionService) }
         List<D> results = doListInternal("from $entity where $idName in 
(:ids)" as String, [ids: convertedIds], [], [:], false)
         Map<Object, D> byId = results.collectEntries { [(it[idName]): it] }
-        ids.collect { byId[it] }
+        convertedIds.collect { byId[it] }
     }
 
     @Override
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiFindWhereSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiFindWhereSpec.groovy
new file mode 100644
index 0000000000..228d0dde64
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiFindWhereSpec.groovy
@@ -0,0 +1,82 @@
+/*
+ *  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 org.grails.orm.hibernate
+
+import java.util.Locale
+
+import grails.gorm.annotation.Entity
+import grails.gorm.transactions.Rollback
+import org.grails.datastore.mapping.core.DatastoreUtils
+import org.grails.orm.hibernate.cfg.Settings
+import org.hibernate.resource.jdbc.spi.StatementInspector
+import org.springframework.transaction.PlatformTransactionManager
+import spock.lang.AutoCleanup
+import spock.lang.Shared
+import spock.lang.Specification
+
+class HibernateGormStaticApiFindWhereSpec extends Specification {
+
+    @Shared SqlCapture sqlCapture = new SqlCapture()
+
+    @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new 
HibernateDatastore(
+            DatastoreUtils.createPropertyResolver(
+                    (Settings.SETTING_DB_CREATE): 'create-drop',
+                    'hibernate.session_factory.statement_inspector': sqlCapture
+            ),
+            FindWhereLimitEntity
+    )
+    @Shared PlatformTransactionManager transactionManager = 
hibernateDatastore.getTransactionManager()
+
+    @Rollback
+    void 'findWhere limits duplicate matches to one row'() {
+        given:
+        new FindWhereLimitEntity(name: 'duplicate').save(flush: true, 
failOnError: true)
+        new FindWhereLimitEntity(name: 'duplicate').save(flush: true, 
failOnError: true)
+        sqlCapture.clear()
+
+        when:
+        FindWhereLimitEntity result = FindWhereLimitEntity.findWhere(name: 
'duplicate')
+
+        then:
+        result.name == 'duplicate'
+        sqlCapture.statements.any { String sql ->
+            String normalized = sql.toLowerCase(Locale.ENGLISH)
+            normalized.contains('where') && normalized.contains('fetch first') 
&& normalized.contains('rows only')
+        }
+    }
+
+    static class SqlCapture implements StatementInspector {
+        final List<String> statements = Collections.synchronizedList(new 
ArrayList<String>())
+
+        @Override
+        String inspect(String sql) {
+            statements.add(sql)
+            return sql
+        }
+
+        void clear() {
+            statements.clear()
+        }
+    }
+}
+
+@Entity
+class FindWhereLimitEntity {
+    String name
+}
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy
index 3c3dc32db9..4074709e88 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy
@@ -22,7 +22,7 @@ package org.grails.orm.hibernate
 
 import grails.gorm.specs.HibernateGormDatastoreSpec
 import grails.gorm.annotation.Entity
-import grails.gorm.specs.entities.Club
+import grails.gorm.tests.entities.Club
 
 class HibernateGormStaticApiSpec extends HibernateGormDatastoreSpec {
 
@@ -185,6 +185,50 @@ class HibernateGormStaticApiSpec extends 
HibernateGormDatastoreSpec {
         instances.size() == 2
     }
 
+    void "Test findWhere matches null values"() {
+        given:
+        new HibernateGormStaticApiEntity(name: "null-test", nullableName: 
null).save(failOnError: true)
+        new HibernateGormStaticApiEntity(name: "other", nullableName: 
"present").save(flush: true, failOnError: true)
+
+        when:
+        def instance = HibernateGormStaticApiEntity.findWhere(nullableName: 
null)
+
+        then:
+        instance.name == 'null-test'
+    }
+
+    void "Test findAllWhere matches null values"() {
+        given:
+        new HibernateGormStaticApiEntity(name: "null-test-1", nullableName: 
null).save(failOnError: true)
+        new HibernateGormStaticApiEntity(name: "null-test-2", nullableName: 
null).save(failOnError: true)
+        new HibernateGormStaticApiEntity(name: "other", nullableName: 
"present").save(flush: true, failOnError: true)
+
+        when:
+        def instances = 
HibernateGormStaticApiEntity.findAllWhere(nullableName: null)
+
+        then:
+        instances.size() == 2
+        instances*.name.containsAll(['null-test-1', 'null-test-2'])
+    }
+
+    void "Test findWhere rejects unsafe property names"() {
+        when:
+        HibernateGormStaticApiEntity.findWhere(['name) or 1=1 or (name': 
'test'])
+
+        then:
+        def e = thrown(IllegalArgumentException)
+        e.message.contains('not a valid property')
+    }
+
+    void "Test findAllWhere rejects unsafe null-valued property names"() {
+        when:
+        HibernateGormStaticApiEntity.findAllWhere(['nullableName) is null or 
1=1 or (nullableName': null])
+
+        then:
+        def e = thrown(IllegalArgumentException)
+        e.message.contains('not a valid property')
+    }
+
     void "Test findAll with HQL using named params"() {
         given:
         new HibernateGormStaticApiEntity(name: "test1").save(failOnError: true)
@@ -519,6 +563,22 @@ class HibernateGormStaticApiSpec extends 
HibernateGormDatastoreSpec {
         instances[2].id == e2.id
     }
 
+    void "Test getAll preserves input order for convertible ids"() {
+        given:
+        def e1 = new HibernateGormStaticApiEntity(name: 
"first").save(failOnError: true)
+        def e2 = new HibernateGormStaticApiEntity(name: 
"second").save(failOnError: true)
+        def e3 = new HibernateGormStaticApiEntity(name: "third").save(flush: 
true, failOnError: true)
+
+        when: "ids are supplied as strings in reverse order"
+        def instances = HibernateGormStaticApiEntity.getAll([e3.id.toString(), 
e1.id.toString(), e2.id.toString()])
+
+        then: "results are ordered by the converted requested ids"
+        instances.size() == 3
+        instances[0].id == e3.id
+        instances[1].id == e1.id
+        instances[2].id == e2.id
+    }
+
     void "Test getAll returns null in position for non-existent ids"() {
         given:
         def e1 = new HibernateGormStaticApiEntity(name: "exists").save(flush: 
true, failOnError: true)
@@ -877,10 +937,14 @@ class HibernateGormStaticApiSpec extends 
HibernateGormDatastoreSpec {
 @Entity
 class HibernateGormStaticApiEntity {
     String name
+    String nullableName
+
+    static constraints = {
+        nullableName nullable: true
+    }
 }
 
 @Entity
 class HibernateGormStaticApiMultiTenantEntity implements 
grails.gorm.MultiTenant<HibernateGormStaticApiMultiTenantEntity> {
     String name
 }
-

Reply via email to