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

jamesfredley pushed a commit to branch test/static-api-datasource-routing
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 33166948568722b02f31232ab93ed0c18137bdf8
Author: James Fredley <[email protected]>
AuthorDate: Sat Feb 21 20:10:32 2026 -0500

    test: add multi-datasource static API routing tests across unit, TCK, and 
functional test suites
    
    Verify that GORM static methods (executeQuery, withCriteria, createCriteria,
    executeUpdate, withTransaction) route to the correct datasource for entities
    mapped to non-default datasources. Covers the allQualifiers() fix in 
4e04e96.
    
    Assisted-by: Claude Code <[email protected]>
---
 .../MultipleDataSourceConnectionsSpec.groovy       | 65 +++++++++++++++-
 .../gorm/GormEnhancerAllQualifiersSpec.groovy      | 46 ++++++++++++
 .../functionaltests/DatasourceSwitchingSpec.groovy | 86 ++++++++++++++++++++++
 3 files changed, 194 insertions(+), 3 deletions(-)

diff --git 
a/grails-data-hibernate5/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceConnectionsSpec.groovy
 
b/grails-data-hibernate5/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceConnectionsSpec.groovy
index 7dce55f315..ee8ee638e9 100644
--- 
a/grails-data-hibernate5/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceConnectionsSpec.groovy
+++ 
b/grails-data-hibernate5/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceConnectionsSpec.groovy
@@ -112,6 +112,68 @@ class MultipleDataSourceConnectionsSpec extends 
Specification {
         }
     }
 
+    void "static GORM operations use first non-default datasource for multi 
datasource entity"() {
+        given: "a unique book name"
+        def uniqueName = "The Stand ${UUID.randomUUID()}"
+
+        when: "saving a book to the books datasource"
+        Book.withTransaction {
+            new Book(name: uniqueName).save(flush: true)
+        }
+
+        then: "withNewSession uses books datasource"
+        Book.withNewSession { Session s ->
+            assert s.connection().metaData.getURL() == "jdbc:h2:mem:books"
+            return true
+        }
+
+        when: "executing a static query"
+        def books = Book.withTransaction {
+            Book.executeQuery("from Book where name = :name", [name: 
uniqueName])
+        }
+
+        then: "the books datasource is queried"
+        books.size() == 1
+
+        when: "executing criteria query"
+        def criteriaResults = Book.withTransaction {
+            Book.withCriteria {
+                eq('name', uniqueName)
+            }
+        }
+
+        then: "criteria uses the books datasource"
+        criteriaResults.size() == 1
+
+        when: "executing update"
+        def updatedName = "The Stand Updated ${UUID.randomUUID()}"
+        int updated = Book.withTransaction {
+            Book.executeUpdate("update Book set name = :name where name = 
:oldName", [name: updatedName, oldName: uniqueName])
+        }
+
+        then: "update affects the books datasource"
+        updated == 1
+        Book.withTransaction { Book.findByName(updatedName) } != null
+
+        when: "executing a static transaction"
+        int count = Book.withTransaction {
+            Book.countByName(updatedName)
+        }
+
+        then: "transaction uses the books datasource"
+        count == 1
+    }
+
+    void "ALL mapped entity uses default datasource for withNewSession"() {
+        when: "requesting a new session for ALL mapped entity"
+        def url = Author.withNewSession { Session s ->
+            s.connection().metaData.getURL()
+        }
+
+        then: "default datasource is used"
+        url == "jdbc:h2:mem:grailsDB"
+    }
+
     void "test @Transactional with connection property to non-default 
database"() {
 
         when:
@@ -159,6 +221,3 @@ class TestService {
 
     def doSomething() {}
 }
-
-
-
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerAllQualifiersSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerAllQualifiersSpec.groovy
index a27a9693c2..85a7370821 100644
--- 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerAllQualifiersSpec.groovy
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerAllQualifiersSpec.groovy
@@ -102,6 +102,38 @@ class GormEnhancerAllQualifiersSpec extends Specification {
         qualifiers == ['secondary']
     }
 
+    void "registerEntity adds static api under default and secondary for 
non-default datasource"() {
+        given: "a non-MultiTenant entity with datasource 'secondary'"
+        def enhancer = createEnhancer()
+        def entity = mockEntity(NonMultiTenantSecondaryEntity, ['secondary'])
+        def staticApis = GormEnhancer.@STATIC_APIS
+        staticApis.get(ConnectionSource.DEFAULT).remove(entity.name)
+        staticApis.get('secondary').remove(entity.name)
+
+        when: "registering the entity"
+        enhancer.registerEntity(entity)
+
+        then: "static api is available under DEFAULT and secondary qualifiers"
+        staticApis.get(ConnectionSource.DEFAULT).containsKey(entity.name)
+        staticApis.get('secondary').containsKey(entity.name)
+    }
+
+    void "registerEntity adds static api under default and secondary for 
MultiTenant entity"() {
+        given: "a MultiTenant entity with datasource 'secondary'"
+        def enhancer = createEnhancer()
+        def entity = mockEntity(MultiTenantSecondaryEntity, ['secondary'])
+        def staticApis = GormEnhancer.@STATIC_APIS
+        staticApis.get(ConnectionSource.DEFAULT).remove(entity.name)
+        staticApis.get('secondary').remove(entity.name)
+
+        when: "registering the entity"
+        enhancer.registerEntity(entity)
+
+        then: "static api is available under DEFAULT and secondary qualifiers"
+        staticApis.get(ConnectionSource.DEFAULT).containsKey(entity.name)
+        staticApis.get('secondary').containsKey(entity.name)
+    }
+
     void "MultiTenant entity with default datasource expands to all 
qualifiers"() {
         given: "a MultiTenant entity on the default datasource"
         def enhancer = createEnhancer()
@@ -159,6 +191,20 @@ class GormEnhancerAllQualifiersSpec extends Specification {
         qualifiers == [ConnectionSource.DEFAULT]
     }
 
+    void "registerEntity adds static api under default for default 
datasource"() {
+        given: "a non-MultiTenant entity on the default datasource"
+        def enhancer = createEnhancer()
+        def entity = mockEntity(NonMultiTenantDefaultEntity, 
[ConnectionSource.DEFAULT])
+        def staticApis = GormEnhancer.@STATIC_APIS
+        staticApis.get(ConnectionSource.DEFAULT).remove(entity.name)
+
+        when: "registering the entity"
+        enhancer.registerEntity(entity)
+
+        then: "static api is available under DEFAULT qualifier"
+        staticApis.get(ConnectionSource.DEFAULT).containsKey(entity.name)
+    }
+
     void "non-MultiTenant entity with ALL datasource expands to all 
qualifiers"() {
         given: "a non-MultiTenant entity declared with ConnectionSource.ALL"
         def enhancer = createEnhancer()
diff --git 
a/grails-test-examples/datasources/src/integration-test/groovy/functionaltests/DatasourceSwitchingSpec.groovy
 
b/grails-test-examples/datasources/src/integration-test/groovy/functionaltests/DatasourceSwitchingSpec.groovy
index f9ddb173d1..55d59ef6f4 100644
--- 
a/grails-test-examples/datasources/src/integration-test/groovy/functionaltests/DatasourceSwitchingSpec.groovy
+++ 
b/grails-test-examples/datasources/src/integration-test/groovy/functionaltests/DatasourceSwitchingSpec.groovy
@@ -367,4 +367,90 @@ class DatasourceSwitchingSpec extends Specification {
         and: "secondary books still exist"
         SecondBook.withTransaction { SecondBook.countByTitleLike("Delete Me%") 
} == 2
     }
+
+    def "executeQuery routes to secondary datasource"() {
+        given: "books in both datasources"
+        def primaryTitle = "Primary Query ${UUID.randomUUID()}"
+        def secondaryTitle = "Secondary Query ${UUID.randomUUID()}"
+        new Book(title: primaryTitle).save(flush: true)
+        SecondBook.withTransaction {
+            new SecondBook(title: secondaryTitle).save(flush: true)
+        }
+
+        when: "executing query on secondary"
+        def results
+        SecondBook.withTransaction {
+            results = SecondBook.executeQuery("from ds2.Book where title = 
:t", [t: secondaryTitle])
+        }
+
+        then: "only secondary data is returned"
+        results.size() == 1
+        results.first().title == secondaryTitle
+    }
+
+    def "withCriteria routes to secondary datasource"() {
+        given: "books in both datasources"
+        def primaryTitle = "Primary Criteria ${UUID.randomUUID()}"
+        def secondaryTitle = "Secondary Criteria ${UUID.randomUUID()}"
+        new Book(title: primaryTitle).save(flush: true)
+        SecondBook.withTransaction {
+            new SecondBook(title: secondaryTitle).save(flush: true)
+        }
+
+        when: "executing criteria on secondary"
+        def results
+        SecondBook.withTransaction {
+            results = SecondBook.withCriteria {
+                eq('title', secondaryTitle)
+            }
+        }
+
+        then: "only secondary data is returned"
+        results.size() == 1
+        results.first().title == secondaryTitle
+    }
+
+    def "createCriteria routes to secondary datasource"() {
+        given: "books in both datasources"
+        def primaryTitle = "Primary CreateCriteria ${UUID.randomUUID()}"
+        def secondaryTitle = "Secondary CreateCriteria ${UUID.randomUUID()}"
+        new Book(title: primaryTitle).save(flush: true)
+        SecondBook.withTransaction {
+            new SecondBook(title: secondaryTitle).save(flush: true)
+        }
+
+        when: "executing createCriteria on secondary"
+        def results
+        SecondBook.withTransaction {
+            results = SecondBook.createCriteria().list {
+                eq('title', secondaryTitle)
+            }
+        }
+
+        then: "only secondary data is returned"
+        results.size() == 1
+        results.first().title == secondaryTitle
+    }
+
+    def "executeUpdate routes to secondary datasource"() {
+        given: "books in both datasources"
+        def primaryTitle = "Primary Update ${UUID.randomUUID()}"
+        def secondaryTitle = "Secondary Update ${UUID.randomUUID()}"
+        def updatedTitle = "Secondary Updated ${UUID.randomUUID()}"
+        new Book(title: primaryTitle).save(flush: true)
+        SecondBook.withTransaction {
+            new SecondBook(title: secondaryTitle).save(flush: true)
+        }
+
+        when: "executing update on secondary"
+        int updated
+        SecondBook.withTransaction {
+            updated = SecondBook.executeUpdate("update ds2.Book set title = 
:newTitle where title = :oldTitle", [newTitle: updatedTitle, oldTitle: 
secondaryTitle])
+        }
+
+        then: "only secondary data is updated"
+        updated == 1
+        SecondBook.withTransaction { SecondBook.findByTitle(updatedTitle) } != 
null
+        Book.findByTitle(updatedTitle) == null
+    }
 }

Reply via email to