This is an automated email from the ASF dual-hosted git repository. jamesfredley pushed a commit to branch fix/detached-criteria-clone in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 3bc530e990ea0f6b9982a3a0d04d587c6c2abd4b Author: James Fredley <[email protected]> AuthorDate: Fri Feb 20 12:31:22 2026 -0500 fix: copy missing fields in AbstractDetachedCriteria.clone() clone() was missing connectionName, lazyQuery, and associationCriteriaMap. This caused withConnection() settings to be lost when clone() was called by subsequent chained methods like max(), offset(), or sort() - each of which internally clones the criteria. Fixes #15422 Assisted-by: Claude Code <[email protected]> --- .../query/criteria/AbstractDetachedCriteria.groovy | 3 + .../criteria/DetachedCriteriaCloneSpec.groovy | 123 +++++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractDetachedCriteria.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractDetachedCriteria.groovy index f04fd50c55..077c0379a8 100644 --- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractDetachedCriteria.groovy +++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractDetachedCriteria.groovy @@ -881,6 +881,9 @@ abstract class AbstractDetachedCriteria<T> implements Criteria, Cloneable { criteria.defaultOffset = defaultOffset criteria.@fetchStrategies = new HashMap<>(this.fetchStrategies) criteria.@joinTypes = new HashMap<>(this.joinTypes) + criteria.@connectionName = this.connectionName + criteria.@lazyQuery = this.lazyQuery + criteria.@associationCriteriaMap = new LinkedHashMap<>(this.associationCriteriaMap) return criteria } diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/query/criteria/DetachedCriteriaCloneSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/query/criteria/DetachedCriteriaCloneSpec.groovy new file mode 100644 index 0000000000..204c091b9e --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/query/criteria/DetachedCriteriaCloneSpec.groovy @@ -0,0 +1,123 @@ +/* + * 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.datastore.gorm.query.criteria + +import grails.gorm.DetachedCriteria +import org.grails.datastore.mapping.core.connections.ConnectionSource +import spock.lang.Specification + +class DetachedCriteriaCloneSpec extends Specification { + + void "clone preserves connectionName"() { + given: + def criteria = new DetachedCriteria(TestEntity) + criteria.@connectionName = 'secondary' + + when: + def cloned = criteria.clone() + + then: + cloned.@connectionName == 'secondary' + } + + void "clone preserves lazyQuery"() { + given: + def lazy = { -> } + def criteria = new DetachedCriteria(TestEntity) + criteria.@lazyQuery = lazy + + when: + def cloned = criteria.clone() + + then: + [email protected](lazy) + } + + void "clone preserves associationCriteriaMap"() { + given: + def criteria = new DetachedCriteria(TestEntity) + def placeholder = 'marker' + criteria.@associationCriteriaMap['books'] = placeholder + + when: + def cloned = criteria.clone() + + then: + [email protected]() == 1 + cloned.@associationCriteriaMap['books'].is(placeholder) + } + + void "clone creates independent copy of associationCriteriaMap"() { + given: + def criteria = new DetachedCriteria(TestEntity) + criteria.@associationCriteriaMap['books'] = 'marker' + + when: + def cloned = criteria.clone() + cloned.@associationCriteriaMap['authors'] = 'another' + + then: + [email protected]() == 1 + [email protected]() == 2 + } + + void "withConnection followed by max preserves connectionName"() { + given: + def criteria = new DetachedCriteria(TestEntity) + + when: + def withConn = criteria.withConnection('secondary') + def withMax = withConn.max(10) + + then: + withConn.@connectionName == 'secondary' + withMax.@connectionName == 'secondary' + withMax.defaultMax == 10 + } + + void "clone preserves default connectionName"() { + given: + def criteria = new DetachedCriteria(TestEntity) + + when: + def cloned = criteria.clone() + + then: + cloned.@connectionName == ConnectionSource.DEFAULT + } + + void "clone preserves existing fields"() { + given: + def criteria = new DetachedCriteria(TestEntity) + criteria.@defaultMax = 50 + criteria.@defaultOffset = 10 + + when: + def cloned = criteria.clone() + + then: + cloned.defaultMax == 50 + cloned.defaultOffset == 10 + } +} + +class TestEntity { + Long id + String name +}
