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