fix: GormEnhancer.allQualifiers() overrides explicit datasource declarations for MultiTenant entities#15393
Conversation
… in allQualifiers() GormEnhancer.allQualifiers() unconditionally expanded qualifiers to all connection sources for MultiTenant entities, even when the entity declared an explicit non-default datasource (e.g., datasource 'secondary'). This caused silent data routing to the wrong database under DISCRIMINATOR multi-tenancy mode. The fix checks whether the entity has an explicit non-default, non-ALL datasource declaration before expanding qualifiers. When present, the explicit mapping is preserved. Added 7 Spock tests covering all combinations: - MultiTenant with explicit/default/ALL datasource - Non-MultiTenant with explicit/default/ALL datasource - MultiTenant with multiple explicit datasources
jdaugherty
left a comment
There was a problem hiding this comment.
I worry mocking APIs in GORM unit tests will be unmaintainable in the longer run - since you are mocking just that its called in a certain way. You already have a functional test for this scenario, why not add it too?
jdaugherty
left a comment
There was a problem hiding this comment.
I thought I had submitted a review for this, but I may have gotten it mixed up with so many PRs being opened.
The test is highly relying on mocking, which is not a reliable approach long term. We need to at least add a functional test for this scenario (it sounds like you already have one too).
If we add the functional test, I'm good to merge this with teh mocking, otherwise we need to rework the unit test to not be so fragile.
… + secondary datasource Add 7 Spock tests covering GORM Data Service CRUD methods when both DISCRIMINATOR multi-tenancy and a non-default datasource are configured on the same domain entity. This is the exact combination that triggers the allQualifiers() bug fixed in this branch. Tests cover: schema creation on correct datasource, save/get/delete/ count routing via @transactional(connection), findByName with tenant isolation, and GormEnhancer escape-hatch HQL on secondary datasource. Assisted-by: OpenCode <opencode@opencode.ai> Assisted-by: Claude Opus 4 <claude@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes qualifier expansion in GormEnhancer.allQualifiers() so that MultiTenant domain classes with an explicit non-default datasource mapping (e.g. datasource 'secondary') keep that qualifier instead of being forcibly expanded to DEFAULT + all connections, preventing silent routing to the wrong database in DISCRIMINATOR multi-tenancy mode.
Changes:
- Update
GormEnhancer.allQualifiers()to preserve explicit non-default datasource qualifiers forMultiTenantentities. - Add unit-level Spock coverage for
allQualifiers()across MultiTenant/non-MultiTenant and explicit/default/ALL mappings. - Add an integration-style Hibernate spec intended to exercise Data Services with DISCRIMINATOR multi-tenancy + secondary datasource.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEnhancer.groovy |
Adjusts qualifier expansion logic to avoid overriding explicit non-default datasource mappings for MultiTenant entities. |
grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerAllQualifiersSpec.groovy |
Adds focused unit tests validating the new allQualifiers() behavior across key combinations. |
grails-data-hibernate5/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiTenantMultiDataSourceSpec.groovy |
Adds a Hibernate-backed spec around Data Services + multi-tenancy + secondary datasource behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
...groovy/org/grails/orm/hibernate/connections/DataServiceMultiTenantMultiDataSourceSpec.groovy
Outdated
Show resolved
Hide resolved
...groovy/org/grails/orm/hibernate/connections/DataServiceMultiTenantMultiDataSourceSpec.groovy
Show resolved
Hide resolved
...groovy/org/grails/orm/hibernate/connections/DataServiceMultiTenantMultiDataSourceSpec.groovy
Outdated
Show resolved
Hide resolved
9a6f246 to
fe2682b
Compare
…nant + multi-datasource Add grails-multitenant-multi-datasource functional test app with 7 integration tests covering DISCRIMINATOR multi-tenancy combined with a non-default datasource. Tests verify save, get, delete, count, findByName, findAllByName, and schema creation all route correctly to the secondary datasource with tenant isolation. Address Copilot review feedback on DataServiceMultiTenantMultiDataSourceSpec: - Add datasource verification assertion on save test - Add GormEnhancer qualifier resolution test for unqualified path - Fix @see reference to existing spec Assisted-by: OpenCode <opencode@opencode.ai> Assisted-by: Claude <claude@anthropic.com>
9e030a8 to
58b5fe2
Compare
….xml Register grails-multitenant-multi-datasource test app in the root settings.gradle so it builds in CI, and add logback.xml matching the pattern used by other hibernate5 functional test apps. Assisted-by: OpenCode <opencode@opencode.ai> Assisted-by: Claude <claude@anthropic.com>
|
Updated with Functional test and PR feedback |
…iTenant routing, and CRUD connection fixes Add documentation reflecting the post-fix state after PRs apache#15393, apache#15395, and apache#15396 are merged: - Add @CompileStatic + injected @service property example (PR apache#15396) - Add Multi-Tenancy with explicit datasource section (PR apache#15393) - List all CRUD methods that respect connection routing (PR apache#15395) - Soften IMPORTANT boxes to NOTE with authoritative tone Assisted-by: Claude Code <Claude@Claude.ai>
matrei
left a comment
There was a problem hiding this comment.
This was a pretty significant bug! Nice catch.
|
@matrei Thank you for the test cleanup. |
…iTenant routing, and CRUD connection fixes Add documentation reflecting the post-fix state after PRs apache#15393, apache#15395, and apache#15396 are merged: - Add @CompileStatic + injected @service property example (PR apache#15396) - Add Multi-Tenancy with explicit datasource section (PR apache#15393) - List all CRUD methods that respect connection routing (PR apache#15395) - Soften IMPORTANT boxes to NOTE with authoritative tone Assisted-by: Claude Code <Claude@Claude.ai>
Summary
GormEnhancer.allQualifiers()unconditionally expands qualifiers to all connection sources for anyMultiTenantentity, even when the entity declares an explicit non-default datasource (e.g.,datasource 'secondary'). This causes silent data routing to the wrong database under DISCRIMINATOR multi-tenancy mode.The fix preserves explicit datasource qualifiers for
MultiTenantentities instead of replacing them withDEFAULT + all connections.Root Cause
In
GormEnhancer.allQualifiers(), theMultiTenantcheck unconditionally triggers qualifier expansion:This treats
MultiTenantthe same asConnectionSource.ALL. It is intended for DATABASE multi-tenancy mode (each tenant = separate connection), but incorrectly fires for DISCRIMINATOR mode where all tenants share one database and the entity has an explicit non-default datasource declaration.When
findTenantId()returnsConnectionSource.DEFAULTin DISCRIMINATOR mode,findStaticApi()resolves to the DEFAULT qualifier - routing all data to the wrong database with no error or warning.Fix
A single conditional change in
GormEnhancer.allQualifiers()(grails-datamapping-core):Qualifier expansion now only fires for:
MultiTenantentities on the default datasource (DATABASE multi-tenancy)ConnectionSource.ALLWhen a
MultiTenantentity has an explicit non-default datasource (e.g.,datasource 'secondary'), the declared qualifier is preserved.Steps To Reproduce
MultiTenantwithdatasource 'secondary'.save()on an instance of that domain classStandalone reproducer: https://github.com/jamesfredley/grails-multitenant-datasource-bug
Test Coverage
21 tests across 3 levels:
Unit tests -
GormEnhancerAllQualifiersSpec(7 tests)Focused tests for
allQualifiers()logic using mock datastores and entities:ConnectionSource.ALL- expands to all qualifiersConnectionSource.ALL- expands to all qualifiersHibernate integration tests -
DataServiceMultiTenantMultiDataSourceSpec(7 tests)Full Hibernate datastore tests with two in-memory H2 databases (default + analytics):
Functional test app -
MultiTenantMultiDataSourceSpec(7 tests)Full Grails application (
grails-multitenant-multi-datasource) with@Integrationtests:Changed Files
grails-datamapping-core/.../GormEnhancer.groovygrails-datamapping-core/.../GormEnhancerAllQualifiersSpec.groovyallQualifiers()grails-data-hibernate5/.../DataServiceMultiTenantMultiDataSourceSpec.groovygrails-test-examples/.../grails-multitenant-multi-datasource/settings.gradleEnvironment Information