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