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

borinquenkid pushed a commit to branch feat/gorm-registry-consolidated
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 494af9f163658c5d89b6aa98ef9f261c05ee8a38
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Sat Aug 15 11:06:18 2026 -0500

    Address remaining #16066 review feedback: document Tenants.withId erasure, 
add junction-factory tests
    
    Tenants.withId(Class<? extends Datastore>, ..) was removed but its 
replacement,
    withId(Class domainClass, ..), shares the same erased signature, so old 
call sites
    still compile and silently resolve against the DEFAULT datastore instead of 
throwing.
    Documents the change and migration path in upgrading80x.adoc #45.
    
    Adds H7-only tests for 
HibernateQuery.disjunction()/conjunction()/negation() that
    assert actual result sets rather than criteria shape, covering the same 
junction-drop
    root cause as the countByXOrY fix but for the conjunction/negation paths 
that no
    built-in dynamic finder exercises. H5's HibernateQuery hierarchy never had 
this bug
    (AbstractHibernateQuery doesn't share H7's junction-factory 
implementation), so there
    is no H5-side equivalent to add.
    
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
---
 .../tests/hibernatequery/HibernateQuerySpec.groovy | 41 +++++++++++++++++++++
 .../src/en/guide/upgrading/upgrading80x.adoc       | 43 ++++++++++++++++++++++
 2 files changed, 84 insertions(+)

diff --git 
a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hibernatequery/HibernateQuerySpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hibernatequery/HibernateQuerySpec.groovy
index 218af9917d..54c6d215e7 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hibernatequery/HibernateQuerySpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hibernatequery/HibernateQuerySpec.groovy
@@ -963,6 +963,47 @@ class HibernateQuerySpec extends 
HibernateGormDatastoreSpec {
         bob == oldBob
     }
 
+    def "countByXOrY exercises disjunction() through a dynamic finder"() {
+        given: "one person matching only the age side of the Or, one matching 
only the name side, and one matching neither"
+        new Person(firstName: "Fred", lastName: "Rogers", age: 51).save(flush: 
true)
+        new Person(firstName: "Walt", lastName: "Disney", age: 52).save(flush: 
true)
+
+        when: "counting by a condition oldBob satisfies through age and Fred 
satisfies through firstName"
+        def count = Person.countByFirstNameOrAge("Fred", 50)
+
+        then: "only the two matching rows are counted; a disjunction dropped 
onto the unused base criteria field would count every row"
+        count == 2
+    }
+
+    def "conjunction() combines added criteria so only rows matching all of 
them are returned"() {
+        given: "a second person who satisfies only one of the two criteria 
added to the conjunction"
+        new Person(firstName: "Fred", lastName: "Rogers", age: 50).save(flush: 
true)
+
+        when: "adding two criteria directly to the Junction returned by 
conjunction()"
+        def conjunction = hibernateQuery.conjunction()
+        conjunction.add(new Query.Equals("firstName", "Bob"))
+        conjunction.add(new Query.Equals("age", 50))
+        def results = hibernateQuery.list()
+
+        then: "only the row matching both criteria is returned; a conjunction 
dropped onto the unused base criteria field would return every row"
+        results.size() == 1
+        results[0] == oldBob
+    }
+
+    def "negation() excludes rows matching the added criterion"() {
+        given: "a second person who would incorrectly remain excluded if the 
negation were silently dropped"
+        def fred = new Person(firstName: "Fred", lastName: "Rogers", age: 
51).save(flush: true)
+
+        when: "negating a criterion that matches oldBob"
+        def negation = hibernateQuery.negation()
+        negation.add(new Query.Equals("firstName", "Bob"))
+        def results = hibernateQuery.list()
+
+        then: "oldBob is excluded and only the non-matching row remains; a 
negation dropped onto the unused base criteria field would return every row"
+        results.size() == 1
+        results[0] == fred
+    }
+
     def andList() {
         given:
         hibernateQuery.and([new Query.Equals("firstName", "Bob"), new 
Query.Equals("age", 50)])
diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc 
b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
index 0c354b2945..5711e47408 100644
--- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
+++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
@@ -2327,3 +2327,46 @@ CurrentTenantHolder.get(datastore)
 ----
 
 See <<multiTenancy,Multi-Tenancy>> for the full multi-tenancy API.
+
+===== Tenants.withId(Class, Serializable, Closure)
+
+`grails.gorm.multitenancy.Tenants` had two distinct 3-argument `withId` 
overloads that both erased to
+`withId(Class, Serializable, Closure)` at the bytecode level:
+
+[source,groovy]
+----
+// removed - resolved a datastore by its own type
+static <T> T withId(Class<? extends Datastore> datastoreClass, Serializable 
tenantId, Closure<T> callable)
+
+// kept - resolves the datastore that maps a domain class
+static <T> T withId(Class domainClass, Serializable tenantId, Closure<T> 
callable)
+----
+
+The datastore-class overload relied on 
`GormEnhancer.findDatastoreByType(Class)`, which was one of the internal
+per-method-dispatch helpers removed as part of the registry migration (see 
<<_gormenhancer,GormEnhancer>>
+above), so it could not be carried forward under its own name. Only the 
domain-class overload remains.
+
+Because both overloads share one erased signature, a call site that used to 
pass a datastore class -
+`Tenants.withId(SomeDatastore, tenantId) { ... }` - still compiles unchanged 
against the domain-class overload.
+It no longer resolves the datastore you named: 
`getDatastoreForDomain(SomeDatastore)` looks `SomeDatastore` up as
+if it were a *domain class*, finds no entity registered under that name, and 
falls through to the application's
+`DEFAULT` datastore. On a single-datastore application this is 
indistinguishable from the old behaviour. On a
+multi-datastore application it silently binds the tenant against the wrong 
datastore - no exception, no log -
+which can surface much later as a cross-database read or write.
+
+If you have a call site that passes a `Datastore` subclass to this method, 
migrate it to look the datastore up
+explicitly and call the datastore-instance overload instead. 
`Tenants.datastoreLocator` still resolves a
+datastore by its type - only the `withId(Class, ..)` shortcut for it is gone:
+
+[source,groovy]
+----
+MultiTenantCapableDatastore datastore = 
Tenants.datastoreLocator.getDatastore(SomeDatastore) as 
MultiTenantCapableDatastore
+Tenants.withId(datastore, tenantId) { ... }
+----
+
+or pass an actual domain class if that is what you meant:
+
+[source,groovy]
+----
+Tenants.withId(SomeDomainClass, tenantId) { ... }
+----

Reply via email to