Skip to content

(2/3) feat: GORM O(M+N) scaling — GormRegistry, GormEnhancer, and core API refactor#15780

Open
borinquenkid wants to merge 65 commits into
feat/gorm-datastore-infrafrom
feat/gorm-registry-core-impl
Open

(2/3) feat: GORM O(M+N) scaling — GormRegistry, GormEnhancer, and core API refactor#15780
borinquenkid wants to merge 65 commits into
feat/gorm-datastore-infrafrom
feat/gorm-registry-core-impl

Conversation

@borinquenkid

@borinquenkid borinquenkid commented Jun 27, 2026

Copy link
Copy Markdown
Member

📖 Reading order: 2 of 3 — this GormRegistry O(M+N) scaling change is split across three PRs; please review them in order:

  1. (1/3) (1/3) feat: add SessionResolver infrastructure and extend core datastore APIs #15779 — SessionResolver infrastructure & core datastore API extensions
  2. (2/3) (2/3) feat: GORM O(M+N) scaling — GormRegistry, GormEnhancer, and core API refactor #15780 — GormRegistry implementation (core production code)
  3. (3/3) (3/3) test: add core-class unit tests for the GormRegistry O(M+N) rewrite #15790 — core-class unit tests

You are reading (2/3).


Summary

Introduces the GormRegistry singleton that replaces the O(M×N) static map allocation in GormEnhancer. APIs are registered once at entity-registration time and looked up in O(1).

  • GormRegistry — singleton keyed by (entityClass, qualifier); handles MultiTenant qualifier expansion, thread-local preferred datastore, and concurrent-safe removal on close()
  • GormApiFactory / DefaultGormApiFactory — pluggable factory per datastore type; adapters override this to supply typed API instances
  • GormApiResolver — routes static/instance/validation API lookups through the registry with fallback to the default datastore
  • ConnectionSourceNameResolver — extracts and normalises connection-source names from a datastore without leaking ConnectionSourcesSupport internals
  • GormEnhancer — delegates all registration and lookup to GormRegistry; allQualifiers() used only for datastore routing, not eager API allocation
  • GormStaticApi / GormInstanceApi / GormValidationApi — use DatastoreResolver instead of holding a direct Datastore reference; support qualifier-aware execution via executeQualified()
  • AbstractGormApi.execute() — distinguishes datasource connection qualifiers from tenant-ID qualifiers to avoid overwriting the active tenant context
  • CurrentTenantHolder — thread-safe tenant binding for DISCRIMINATOR multi-tenancy
  • ServiceTransformation / TransactionalTransform — resolve transaction manager via GormRegistry instead of static map lookups
  • DefaultTransactionTemplateFactory / TransactionTemplateFactory — pluggable transaction template creation per datastore type

Test plan

  • ./gradlew :grails-datamapping-core:test passes (tests are in the companion PR)
  • No regressions in GormEnhancerAllQualifiersSpec

Stack

🤖 Generated with Claude Code

@borinquenkid

Copy link
Copy Markdown
Member Author

Note on gradle/test-config.gradle — GORM modules run with maxParallelForks = 1.

The dependsOnProject(...) helper forces single-fork test execution for any module that depends (directly or transitively) on grails-datamapping-core.

Rationale: GormRegistry is a process-wide singleton. With maxParallelForks > 1, sibling specs in the same module share a JVM and can leak registry state across each other (stale entity→datastore/api bindings), producing order-dependent flakiness — e.g. a schema-tenancy spec resolving a datastore bound by an unrelated prior spec. Serializing GORM-dependent test tasks removes that cross-spec pollution deterministically.

This is a known trade-off: it increases wall-clock time for those modules. It is intentional and scoped narrowly to GORM-dependent projects only — non-GORM modules keep the configured parallelism. A finer-grained alternative (per-spec registry reset in setup/cleanup) can supersede this later without changing the public API.

@jamesfredley

Copy link
Copy Markdown
Contributor

@borinquenkid does #15771 fully replace this PR? And then does that remove the need for #15790

@jdaugherty

Copy link
Copy Markdown
Contributor

@borinquenkid I'm not sure I follow all of these PRs. There have been multiple opened and then closed. Can you help me understand what is still valid vs what needs closed and what order they're expected to be reviewed in? I don't see an index anywhere of expected review order.

@borinquenkid borinquenkid changed the title feat: GORM O(M+N) scaling — GormRegistry, GormEnhancer, and core API refactor (2/3) feat: GORM O(M+N) scaling — GormRegistry, GormEnhancer, and core API refactor Jul 1, 2026
Copilot AI review requested due to automatic review settings July 2, 2026 16:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors core GORM API registration and lookup to use a centralized GormRegistry/GormApiResolver + pluggable GormApiFactory model, reducing eager per-qualifier API allocation and enabling qualifier-aware resolution (connections and multi-tenancy) at call time across core and adapter modules.

Changes:

  • Introduces new registry/factory/resolver infrastructure for static/instance/validation APIs and transaction templates.
  • Updates core GORM traits, validation, AST transforms, dynamic finders, criteria/query paths, and multi-tenancy listeners to route through the registry and resolver abstractions.
  • Wires adapter-specific API factories (Hibernate 5/7, MongoDB) and adjusts impacted specs/build/test configuration.

Reviewed changes

Copilot reviewed 108 out of 108 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/UserRoleIntegrationSpec.groovy Filters stdout capture to count only SQL lines
grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/TagIntegrationSpec.groovy Filters stdout capture to count only SQL lines
grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/CommentIntegrationSpec.groovy Filters stdout capture to count only SQL lines
grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/connections/MultipleConnectionSourceCapableDatastore.java Adds datastore SPI for multi-connection + routing hint
grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/FirstAndLastMethodSpec.groovy Expands pending-feature gating to Hibernate 7 suite
grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/base/GrailsDataTckManager.groovy Simplifies domain class storage; adds deprecated bulk add method
grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormStaticApiWithDatastoreSessionSpec.groovy Adds regression test for withDatastoreSession semantics
grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormStaticApiStringQueryDelegationSpec.groovy Adds regression tests for string-query overload delegation
grails-datamapping-core/src/test/groovy/grails/gorm/services/ServiceTransformSpec.groovy Adjusts reflection lookup expectations in service transform tests
grails-datamapping-core/src/test/groovy/grails/gorm/services/MethodValidationTransformSpec.groovy Excludes trait datastore accessors from @Generated assertions
grails-datamapping-core/src/test/groovy/grails/gorm/services/CompileStaticServiceInjectionSpec.groovy Updates expectations for datastore injection internals
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/listener/ValidationEventListener.groovy Switches validation API lookup to GormRegistry
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/MappingContextTraversableResolver.groovy Adds cascade-validation short-circuit hook
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorAdapter.groovy Adds cascade-aware validation pathway + thread-local flag
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/constraints/builtin/UniqueConstraint.groovy Routes static API lookup via GormRegistry
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transform/AbstractMethodDecoratingTransformation.groovy Expands excluded method set; avoids re-decoration on locally annotated methods
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transform/AbstractDatastoreMethodDecoratingTransformation.groovy Refactors datastore resolution in AST transform to use registry/api resolver
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transactions/TransactionTemplateFactory.groovy Adds SPI for datastore-specific transaction template creation
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transactions/DefaultTransactionTemplateFactory.groovy Default implementation using GrailsTransactionTemplate
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/UpdateStringQueryImplementer.groovy Adjusts implementer ordering
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindOneStringQueryImplementer.groovy Improves argument-list handling and query-text detection
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindOneInterfaceProjectionStringQueryImplementer.groovy Adjusts implementer ordering for projection variant
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindAllByImplementer.groovy Adds argument/property-type compatibility checks
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractStringQueryImplementer.groovy Adds named-parameter extraction for strict parameter validation
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractServiceImplementer.groovy Updates datastore/service/API lookup generation to use GormRegistry
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/DefaultTransactionService.groovy Adds datastore getter/setter for service wiring
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/DefaultTenantService.groovy Adds datastore getter/setter; recursion guard; improves exception messages
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java Ensures query initialization for more operations; adds list(Closure) helper
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/transform/TenantTransform.groovy Resolves tenant services via registry/api resolver, with service-specific datastore routing
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/TenantDelegatingGormOperations.groovy Adds tenant-wrapped deleteAll overloads
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/MultiTenantEventListener.java Tightens source validation; resolves datastore via registry; improves tenant id handling
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/schema/DefaultSchemaHandler.groovy Adds schema quoting + (currently) extra stderr debug output
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/MultiTenantConnection.groovy Changes close behavior for multi-tenant connection wrapper
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormValidationApiRegistry.groovy New registry for validation APIs
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormValidationApi.groovy Adds resolver-based construction; qualifier support; safer session/event access
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormValidateable.groovy Routes validation API resolution through GormRegistry with clearer failure mode
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormStaticApiRegistry.groovy New registry for static APIs
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormInstanceApiRegistry.groovy New registry for instance APIs
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEntityDirtyCheckable.groovy Uses registry-based instance API lookup with clearer failure mode
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEntity.groovy Routes entity API access through registry; adds deleteAll overloads; hardens missing-property behavior
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEnhancerRegistry.groovy Extracts/centralizes thread-local enhancer state
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormApiFactory.groovy New SPI for per-datastore API object construction
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/ListOrderByFinder.java Refactors order parsing and multi-property ordering; adjusts criteria application
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindOrSaveByFinder.java Adds resolver-based constructors; delegates to updated base classes
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindOrCreateByFinder.java Adds resolver-based constructors
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindByFinder.java Adds resolver-based constructor
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindByBooleanFinder.java Fixes method pattern; adds resolver-based constructor
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindAllByFinder.java Adds resolver-based constructor; adjusts distinct behavior
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindAllByBooleanFinder.java Adds resolver-based constructor; updates docs
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/DynamicFinder.java Adds resolver-based constructor; improves order-by defaulting (identity/composite ids)
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/CountByFinder.java Refactors countBy* execution/build logic; adds resolver-based constructor
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/AbstractFinder.java Adds DatastoreResolver support for call-time datastore selection
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/AbstractFindByFinder.java Adds resolver-based constructor; adjusts generic callback type
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java Exposes lastUpdated property-name lookup (visibility change)
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/DefaultGormApiFactory.groovy Default core API factory + dynamic finder wiring via resolver
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/DatastoreResolver.groovy Introduces DatastoreResolver SPI and corrects prior misplaced content
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/ConnectionSourceNameResolver.groovy Adds helper to extract/normalize connection source names
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractGormApiRegistry.groovy Adds base registry with lazy qualifier API materialization + datastore removal
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractGormApi.groovy Adds resolver/qualifier support; caches reflection results; qualifier-aware execution model
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractDatastoreApi.groovy Moves datastore access to DatastoreResolver
grails-datamapping-core/src/main/groovy/grails/gorm/transactions/GrailsTransactionTemplate.groovy Adds debug logging around execute/rollback flow
grails-datamapping-core/src/main/groovy/grails/gorm/MultiTenant.groovy Routes withTenant/eachTenant through GormRegistry (with non-resolving path for enumeration)
grails-datamapping-core/src/main/groovy/grails/gorm/multitenancy/CurrentTenantHolder.groovy Adds thread-local tenant binding helper for DISCRIMINATOR mode
grails-datamapping-core/src/main/groovy/grails/gorm/DetachedCriteria.groovy Routes session access through registry static API lookup; returns callable result
grails-datamapping-core/src/main/groovy/grails/gorm/CriteriaBuilder.java Adds call(Closure) convenience to execute criteria
grails-datamapping-core/src/main/groovy/grails/gorm/api/GormStaticOperations.groovy Adds deleteAll overloads to static operations API
grails-datamapping-core/build.gradle Adds grails-data-simple test dependency
grails-datamapping-core-test/src/test/groovy/org/grails/datastore/gorm/ExplicitSaveRepersistsSpec.groovy Adds regression spec for explicit save re-persistence
grails-datamapping-core-test/src/test/groovy/org/grails/datastore/gorm/CoreTestSuite.groovy Expands selected TCK classes in suite
grails-data-simple/src/main/groovy/org/grails/datastore/mapping/simple/SimpleMapDatastore.java Improves connection datastore lookup + routing behavior for unit-test mock
grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/DisjunctionQuerySpec.groovy Marks pre-existing MongoDB count-by-OR bug as pending feature
grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/SchemaBasedMultiTenancySpec.groovy Shares datastore + resets registry state for isolation
grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/MultiTenancySpec.groovy Shares datastore + resets registry state for isolation
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormEnhancer.groovy Registers Mongo API factory via GormRegistry
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormApiFactory.groovy Adds Mongo-specific static API factory implementation
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/api/MongoStaticApi.groovy Captures persistent entity + tenancy mode locally
grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancerSpec.groovy Adjusts test annotations for schema tenant entity
grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormValidationApiSpec.groovy Updates validation API lookup to use registry
grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy Adjusts static API access with explicit qualifier
grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy Updates instance API lookup to use registry
grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/GormApiAllocationSpec.groovy Updates allocation assertions to registry allocation checks
grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsIdentityGeneratorSpec.groovy Registers additional domain classes in spec setup
grails-data-hibernate7/core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerCleanupSpec.groovy Replaces reflection-based checks with registry mapping/API assertions
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java Fixes junction creation to operate on the detached criteria backing store
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSession.java Makes retrieveAll preserve requested order/duplicates + type conversion
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy Delegates propertyMissing to base (finder-closure-first behavior)
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy Registers Hibernate 7 API factory via registry
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormApiFactory.groovy Adds Hibernate 7 API factory implementation
grails-data-hibernate5/core/src/test/groovy/org/grails/orm/hibernate/connections/MultiTenantFinderDispatchSpec.groovy Adds regression spec for finder dispatch + session passing under tenancy
grails-data-hibernate5/core/src/test/groovy/org/grails/orm/hibernate/connections/GormApiAllocationSpec.groovy Updates allocation assertions to registry allocation checks
grails-data-hibernate5/core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerCleanupSpec.groovy Replaces reflection-based checks with registry mapping/API assertions
grails-data-hibernate5/core/src/test/groovy/org/apache/grails/data/hibernate5/core/GrailsDataHibernate5TckManager.groovy Simplifies config handling; sets suite flags/transactional defaults
grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateSession.java Makes retrieveAll preserve requested order/duplicates + type conversion
grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy Delegates propertyMissing to base (finder-closure-first behavior)
grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy Registers Hibernate 5 API factory via registry
grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormApiFactory.groovy Adds Hibernate 5 API factory implementation
grails-data-graphql/core/src/test/groovy/org/grails/gorm/graphql/fetcher/manager/GraphQLDataFetcherManagerSpec.groovy Updates test registry setup to use GormRegistry static API registry
gradle/test-config.gradle Adds Gradle-9-safe transitive project dependency check and serializes GORM-dependent tests
Comments suppressed due to low confidence (2)

grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/schema/DefaultSchemaHandler.groovy:64

  • Remove the System.err.println debug output. Emitting SQL text unconditionally to stderr can leak sensitive identifiers into logs and adds noisy output; log.debug(...) already covers trace logging.
    grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/schema/DefaultSchemaHandler.groovy:79
  • Remove the System.err.println debug output in createSchema. Unconditional stderr output is noisy and can expose schema names; log.debug(...) already captures the statement when debug logging is enabled.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.69057% with 455 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.2094%. Comparing base (51157ac) to head (33ec62c).
⚠️ Report is 2 commits behind head on feat/gorm-datastore-infra.

Files with missing lines Patch % Lines
...oovy/org/grails/datastore/gorm/GormRegistry.groovy 80.5014% 24 Missing and 46 partials ⚠️
...ovy/org/grails/datastore/gorm/GormStaticApi.groovy 81.4978% 19 Missing and 23 partials ⚠️
...y/org/grails/datastore/gorm/GormApiResolver.groovy 81.6216% 4 Missing and 30 partials ⚠️
...oovy/org/grails/datastore/gorm/GormEnhancer.groovy 68.8172% 13 Missing and 16 partials ⚠️
...g/grails/datastore/gorm/finders/DynamicFinder.java 19.4444% 27 Missing and 2 partials ⚠️
...org/grails/datastore/gorm/GormValidationApi.groovy 74.2424% 3 Missing and 14 partials ⚠️
...rm/services/transform/ServiceTransformation.groovy 84.6154% 4 Missing and 12 partials ⚠️
...y/org/grails/datastore/gorm/GormInstanceApi.groovy 84.2105% 3 Missing and 12 partials ⚠️
...re/gorm/multitenancy/MultiTenantEventListener.java 48.1482% 8 Missing and 6 partials ⚠️
...y/org/grails/datastore/gorm/AbstractGormApi.groovy 82.0896% 0 Missing and 12 partials ⚠️
... and 31 more
Additional details and impacted files

Impacted file tree graph

@@                         Coverage Diff                         @@
##             feat/gorm-datastore-infra     #15780        +/-   ##
===================================================================
+ Coverage                      49.6254%   50.2094%   +0.5840%     
- Complexity                       16972      17493       +521     
===================================================================
  Files                             2001       2013        +12     
  Lines                            93841      94793       +952     
  Branches                         16437      16682       +245     
===================================================================
+ Hits                             46569      47595      +1026     
+ Misses                           40103      39916       -187     
- Partials                          7169       7282       +113     
Files with missing lines Coverage Δ
...ls/datastore/gorm/mongo/MongoGormApiFactory.groovy 100.0000% <100.0000%> (ø)
...ails/datastore/gorm/mongo/MongoGormEnhancer.groovy 73.6842% <100.0000%> (+4.9342%) ⬆️
...rc/main/groovy/grails/gorm/DetachedCriteria.groovy 80.9210% <100.0000%> (+1.9737%) ⬆️
...grails/datastore/gorm/DefaultGormApiFactory.groovy 100.0000% <100.0000%> (ø)
.../grails/datastore/gorm/GormEnhancerRegistry.groovy 100.0000% <100.0000%> (ø)
...astore/gorm/events/AutoTimestampEventListener.java 65.7609% <100.0000%> (+0.1871%) ⬆️
...s/datastore/gorm/finders/AbstractFindByFinder.java 85.7143% <100.0000%> (+2.3810%) ⬆️
...datastore/gorm/finders/FindAllByBooleanFinder.java 70.0000% <100.0000%> (+12.8571%) ⬆️
...grails/datastore/gorm/finders/FindAllByFinder.java 84.6154% <100.0000%> (+1.2820%) ⬆️
...ls/datastore/gorm/finders/FindByBooleanFinder.java 70.0000% <100.0000%> (+12.8571%) ⬆️
... and 51 more

... and 50 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

borinquenkid added a commit that referenced this pull request Jul 2, 2026
- MultiTenantConnection.close(): the GormRegistry rewrite dropped the
  default-schema restoration before returning a connection to the pool.
  In SCHEMA-per-tenant multi-tenancy this risks cross-tenant schema
  leakage on connection reuse. Restore the try/finally that resets the
  schema before closing the target connection.
- GormValidationApi.getValidator(): the rewrite to resolve the
  MappingContext via the (qualifier-aware) datastore dropped the
  original caching of the resolved Validator, causing repeated
  resolution on every call. Cache it in internalValidator once resolved.
- AbstractStringQueryImplementer: remove a hard-coded substring check
  ("wrong" / "java.lang.String") in the constant-@query branch. It was
  dead code for the tests it appeared to guard (those use GStrings,
  validated separately by QueryStringTransformer) and would incorrectly
  reject any legitimate constant query containing those substrings.
- Remove leftover println/System.err.println debug output in
  GormValidatorAdapter and DefaultSchemaHandler.
- Fix corrupted Javadoc escaping ("in\"" etc.) in AbstractCriteriaBuilder.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
borinquenkid and others added 5 commits July 4, 2026 12:34
…refactor

Introduce GormRegistry singleton replacing O(M×N) static maps in GormEnhancer.
APIs are registered once at entity-registration time and looked up in O(1).

- GormRegistry: singleton keyed by (entityClass, qualifier); handles MultiTenant
  qualifier expansion, thread-local preferred datastore, and concurrent-safe removal
- GormApiFactory / DefaultGormApiFactory: pluggable factory per datastore type
- GormApiResolver: routes static/instance/validation API lookups through the registry
- GormEnhancer: delegates all registration and lookup to GormRegistry
- GormStaticApi / GormInstanceApi / GormValidationApi: use DatastoreResolver instead
  of holding a direct Datastore reference; support qualifier-aware execution
- AbstractGormApi.execute(): distinguishes datasource connection qualifiers from
  tenant-ID qualifiers to avoid overwriting the active tenant context
- CurrentTenantHolder: thread-safe tenant binding for DISCRIMINATOR multi-tenancy
- ServiceTransformation / TransactionalTransform: resolve transaction manager via
  GormRegistry instead of static map lookups

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…efactor

HibernateGormEnhancer (H5 and H7) both declare @OverRide registerConstraints
as a no-op. The scaling commit's GormEnhancer refactor omitted this protected
hook method, making the @OverRide annotation invalid and causing a Java stub
compilation error: "method does not override or implement a method from a
supertype".

Restores the original implementation (loads ConstraintRegistrar via reflection
if present) and calls it from the constructor, consistent with the pre-scaling
behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ropped by scaling refactor

GormEnhancer: restore protected registerConstraints(Datastore) hook that H5/H7
HibernateGormEnhancer override as a no-op. Its absence broke Java stub
generation with "@OverRide … method does not override a supertype method".

MongoStaticApi: restore persistentEntity and multiTenancyMode fields that
GormStaticApi no longer carries after the scaling refactor. Initialise
persistentEntity from the mapping context and multiTenancyMode from
MongoDatastore.getMultiTenancyMode() so wrapFilterWithMultiTenancy and
preparePipeline compile under @CompileStatic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…stractGormApi.execute()

The GORM scaling commit introduced a non-default qualifier path in execute() that
unconditionally called Tenants.withId(datastore, qualifier) for multi-tenant entities.
This was correct for DATABASE mode (qualifier == tenant ID == connection name) but
broke DISCRIMINATOR mode: when a @service with @transactional(connection='secondary')
executed a query, 'secondary' was bound as the current tenant ID instead of the real
tenant from the TenantResolver, causing discriminator filters to match 'secondary' and
return 0 rows.

Fix: probe getDatastoreForConnection(qualifier) to determine whether the qualifier
names a real datasource connection. If it resolves (non-null), it is a connection name
— fall through to executeQualified without touching the tenant context. If it throws or
returns null, the qualifier is a tenant ID (e.g. from withTenant()) — bind it via
Tenants.withId as before.

Update GormRegistrySpec to explicitly stub getDatastoreForConnection(_) >> null on the
DISCRIMINATOR-mode test stub, mirroring real HibernateDatastore behaviour (which throws
ConfigurationException for unknown connection names) and avoiding Spock's covariant-
interface default of returning the stub itself.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…istry

findStaticApi/InstanceApi/ValidationApi: the tenant-lookup at priority-2 only
checked CurrentTenantHolder.  In DATABASE and SCHEMA modes the tenant ID is never
stored there explicitly — it comes from the TenantResolver (e.g. a subdomain or
system-property resolver).  Consult the resolver for those strict modes so that
per-tenant child APIs are selected correctly even when no tenant has been bound
via Tenants.withId().  Guard with TenantNotFoundException propagation so missing
tenants surface as errors rather than silently falling back to the default API.
Also skip the API redirect when tenantId equals 'default' to avoid self-loops.

createStaticApi / createInstanceApi / createValidationApi: replace the caller-
supplied DatastoreResolver with a bound lambda that always returns the specific
Datastore captured at registration time.  The old resolver was evaluated lazily
at call time and could invoke tenant-resolution logic before any tenant context
was active, causing spurious TenantNotFoundException during bootstrapping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
borinquenkid and others added 29 commits July 11, 2026 21:31
…#15780 coverage gap

GormEnhancer.groovy sat at 49.5% patch coverage (47 bad lines): the
deprecated static/protected delegators, the findEntity static helper,
allQualifiers' foreign-datastore branch, and - hardest to reach - the
addStaticMethods/addInstanceMethods closures were untested. Those
closures only execute when a real missing-method/property call goes
through Groovy's ExpandoMetaClass dispatch on the actual entity class,
not when calling the underlying API object directly (as GormStaticApi's
own spec does), so covering them required exercising real dynamic-finder
calls, unresolvable property access, and unresolvable method calls on a
live SimpleMapDatastore-backed entity.

9 tests. The pre-existing GormEnhancerAllQualifiersSpec already covered
allQualifiers' same-datastore path and registerEntity/close's happy
paths thoroughly via Mock-based datastores; this spec fills the
remaining gaps using a real SimpleMapDatastore, since the datastore's
own internal GormEnhancer already registers/enhances the entity against
the GormRegistry singleton, so most tests don't need to construct their
own GormEnhancer at all - only the ones exercising protected/deprecated
instance methods or an explicitly foreign datastore need one.

Line coverage 89.5% (119/133, up from 55.6%), method coverage 39/42 (up
from 21/42). Full module suite green, 0 regressions. codeStyle clean
(test-only change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ing PR #15780 coverage gap

GormValidationApi.groovy sat at 34.8% patch coverage - the worst in the
PR - with 15 of 23 methods fully uncovered: the alternate constructors,
forQualifier/executeQualified, getTransactionManager, the entire
validate(...) flow (flush-mode save/restore, event firing, the
plain/CascadingValidator dispatch, field-filtered errors), and the
getErrors/setErrors/hasErrors/clearErrors pair of code paths (a
GormValidateable instance stores errors on itself; anything else goes
through the datastore's current session).

19 tests. setValidator() proved the key to testing doValidate (private,
reached via the public validate(...) overloads) without needing a real
GORM validation setup - it bypasses getValidator()'s whole resolution
chain, letting each test drive a specific validator implementation
(plain Validator vs grails.gorm.validation.CascadingValidator) directly.
One dead branch found and deliberately left uncovered:
org.grails.datastore.gorm.validation.CascadingValidator extends
grails.gorm.validation.CascadingValidator, so the `else if` branch
checking for the former is unreachable - any real implementation is
already caught by the first `instanceof` check.

Line coverage 93.4% (127/136, up from 34.6%), method coverage 22/23 (up
from 8/23). Full module suite green, 0 regressions. codeStyle clean
(test-only change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
 coverage gap

GormInstanceApi.groovy had no dedicated spec at all - 56.8% patch
coverage, 42 of 61 methods fully uncovered: the alternate constructors,
forQualifier/executeQualified/getTransactionManager, propertyMissing's
connection-source/DynamicAttributes/MissingPropertyException branches,
instanceOf's EntityProxy unwrapping, the full save/insert/delete/attach/
discard/refresh/read/ident surface, and the DirtyCheckable-backed
isDirty/getDirtyPropertyNames/getPersistentValue methods.

26 tests, built around a real SimpleMapDatastore (matching the house
style already used for GormStaticApi/GormRegistry). setValidator() on
the entity's own registered GormValidationApi (fetched via
GormRegistry.instance.getValidationApi(cls) - the same instance
save()'s registry.resolveValidationApi(...) resolves to) proved the
way to force deterministic validation failure, since static
constraints DSL blocks aren't evaluated into a real Validator outside
a full Grails app. Also found and documented (not fixed - lives in a
different file) a quirk in core ValidationException: its own static
newInstance(...) factory always constructs its dynamically-resolved
VALIDATION_EXCEPTION_TYPE, ignoring whatever Class GormInstanceApi's
own validationException field holds as the call receiver.

Line coverage 93.3% (111/119, up from 37.0%), method coverage 53/61
(up from 19/61). Full module suite green, 0 regressions. codeStyle
clean (test-only change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
 coverage gap

AbstractGormApi.groovy was only exercised incidentally through
GormStaticApi/GormInstanceApi/GormValidationApi's own specs (50.7%
patch coverage) - the null-datastore guard in execute(), the
bound-tenant delegation branch when the DEFAULT qualifier is in use,
the reflection-based getMethods()/getExtendedMethods() cataloging, and
the unused ConstantDatastoreResolver helper were all untested.

5 tests. Used a minimal purpose-built AbstractGormApi subclass
(MinimalGormApi, overriding executeQualified to just record its
argument) to test execute()'s tenant-dispatch branch in isolation,
rather than routing through GormStaticApi's own resolution - which
turns out to depend on the GormRegistry *singleton* regardless of which
registry instance the api was constructed with, since
GormStaticApi.executeQualified calls the static
GormRegistry.findStaticApi(...) delegator, not an instance-scoped
lookup. Worth remembering for any future item that needs to control
qualified-api resolution precisely.

Line coverage 98.7% (78/79, up from 59.5%), method coverage 24/24 (up
from 13/24 - 0 methods now fully uncovered). Full module suite green,
0 regressions. codeStyle clean (test-only change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rage gap

Tenants.groovy had no spec at all despite being the primary public API
for multi-tenancy: 50.0% patch coverage, 37 of 47 methods fully
uncovered - most of the thin static wrapper methods around the
swappable Tenants.datastoreLocator field, plus large untested branches
in the two methods with real dispatch logic (withId(MultiTenantCapableDatastore,...)
and eachTenant(MultiTenantCapableDatastore,...)).

37 tests. Swapped Tenants.datastoreLocator for a test-controlled
DatastoreLocator (restored in cleanup()) to deterministically drive
the no-arg/domain-class/type-based locator methods without touching
the GormRegistry singleton. Covered withId's three dispatch paths
(already-bound child session, shared-connection, non-shared
withNewSession) each across their 0/1/2-arg closure-arity branches
plus the too-many-args guard, and eachTenant's four mode/resolver
combinations (DATABASE+AllTenantsResolver, DATABASE+ConnectionSources
iteration, shared+AllTenantsResolver, shared without one -> exception).

Line coverage 95.4% (145/152, up from 23.7% - the largest
baseline-to-result jump of any item so far), method coverage 44/47 (up
from 10/47). Full module suite green, 0 regressions. codeStyle clean
(test-only change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
#15780 coverage gap

DynamicFinder.java had 19.4% Codecov patch coverage (worst remaining item in
the PR #15780 coverage checklist). The pre-existing DynamicFinderSpec only
covered the static buildMatchSpec helper.

Added DynamicFinderCoverageSpec, exercising the real method-name-parsing /
expression-building pipeline through actual dynamic finder calls on a
SimpleMapDatastore-backed entity (Equal, GreaterThan/LessThan/Between,
Like/InList/NotEqual, IsNull/IsNotNull, Not-negation, And/Or combination,
MissingMethodException on arg-count/conversion failures), list(Map) argument
handling (sort as string/Map, fetch as FetchType map/string alias, cache,
no-sort fallback), getFetchMode's alias table, where{}.list() detached
criteria fetch/order, registerNewMethodExpression's custom-clause extension
hook, the MappingContext-only constructor, the invoke(..., DetachedCriteria,
...) overload, and populateArgumentsForCriteria(BuildableCriteria, Map)
directly (confirmed via repo-wide grep to be dead code - no production
caller uses this overload, only the (Class, Query, Map) one - but still a
public static API worth covering).

Notable findings, neither a bug:
- The operator-style where{ age > 20 } DSL relies on a compile-time AST
  transform this plain test-module compilation doesn't apply; the
  method-call DSL (where { gt('age', 20) }) works without it.
- A custom MethodExpression's finder clause keyword is the registered
  class's simple name exactly (e.g. class AlwaysTrue -> findAllByXAlwaysTrue),
  not a suffixed variant.
- CriteriaBuilder instances from a bare createCriteria() (outside an active
  query-execution closure) have a null internal query field, so join()/
  cache() NPE when called directly; populateArgumentsForCriteria(BuildableCriteria,
  Map) is therefore only exercised here via its sort/order branches.

DynamicFinder.java line coverage: 46.5% (194/417) before this item's work ->
74.0% (305/412) after, per local JaCoCo. Full grails-datamapping-core suite
and codeStyle both pass with no regressions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…closing PR #15780 coverage gap

Covers the abstract-class-with-explicit-constructor compile error and the
concrete (non-interface, non-abstract) @service class path, both new in the
GormRegistry rewrite and previously untested from this module's own JaCoCo
perspective.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ng PR #15780 coverage gap

No spec existed for this class at all. Covers the new isValidSource guard,
the DEFAULT+numeric tenant id coercion, and the existing-property-wins
override on insert - all new in the GormRegistry rewrite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rageSpec closing PR #15780 coverage gap

Covers buildNamedParamsFromQuery's named-parameter binding for constant
@query strings, and FindOneStringQueryImplementer's ArgumentListExpression
merge branch that consumes it - both new in the GormRegistry rewrite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…#15780 coverage gap

Covers the mode==NONE guard shared by currentId/withoutId/withCurrent/
withId, each method's delegation to Tenants.*, and the new RESOLVING
reentrancy guard on withCurrent/withId.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…15780 coverage gap

Brand-new file in the GormRegistry rewrite; no dedicated spec existed.
Covers the full public contract including the "restore the previous
binding" branches of withTenant(Class,...)/withoutTenant when nested.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… closing PR #15780 coverage gap

Covers the 4 new deleteAll(...) overloads this PR added, matching the
pre-existing delegation pattern used by the other ~90 methods on this class.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ing PR #15780 coverage gap

Covers executeAndRollback's functional contract (result passthrough,
always-rollback, checked/unchecked exception unwrapping), which had no
coverage of its own before this PR added debug logging around it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ct against a null query

AbstractCriteriaBuilder gained ensureQueryIsInitialized() guards on cache/
join/select in this PR to fix the NPE a bare createCriteria() (not inside
.list{}/.get{}) hit on those methods. CriteriaBuilder overrides all three
with its own versions that touch `query` directly, unguarded, so the NPE
was still reachable through the concrete class real callers actually use
(GormStaticApi#createCriteria() returns a CriteriaBuilder, not the abstract
base). Added the same guard to CriteriaBuilder's own overrides.

Also adds CriteriaBuilderSpec, closing PR #15780's coverage gap on both
AbstractCriteriaBuilder and CriteriaBuilder (the concrete class most real
callers get), including the fix's regression test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
#15780 coverage gap

Brand-new file. Covers qualify()'s null-datastore fallback and
findStaticApi(Class, String)'s own instance method, which GormRegistry's
static findStaticApi delegator does not route through.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…losing PR #15780 coverage gap

Brand-new file. Covers the fallback path for a non-ConnectionSourcesProvider
datastore, the one gap left uncovered by incidental SimpleMapDatastore usage
elsewhere in the suite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…GormInstanceApiRegistrySpec closing PR #15780 coverage gap

Structurally identical to GormStaticApiRegistry (same AbstractGormApiRegistry
subclass pattern) - both brand-new files with no dedicated spec. Covers
qualify()'s null-datastore fallback and each class's own findXApi(Class,
String) instance method.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… coverage gap

Covers the new DatastoreResolver-based constructor + lazy getDatastore(),
which DefaultGormApiFactory#createDynamicFinders uses to construct every
named dynamic finder (FindAllByFinder, CountByFinder, ListOrderByFinder,
etc.) in the GormRegistry rewrite, via real dynamic finder calls against a
SimpleMapDatastore-backed entity.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…losing PR #15780 coverage gap

Covers the new type-compatibility check added to the dynamic-finder
property-validation loop: a finder parameter whose type doesn't match the
matched property's declared type now fails to compile with a clear message,
instead of silently generating a finder that would misbehave at runtime.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…15780 coverage gap

Covers the simplified shouldSaveOnCreate()-based implementation (replacing
a bespoke ~25-line doInvokeInternal override) and the 3 new constructor
overloads added to match sibling finder classes' shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…#15780 coverage gap

Covers the new DatastoreResolver-based constructor + lazy getDatastore()
(returns null instead of throwing when unconfigured), via a minimal
purpose-built subclass since this class's only real subclass,
AbstractGormApi, overrides execute() with its own qualifier-aware version.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…coverage gap

grails-data-simple had no src/test at all - added testImplementation
'org.spockframework:spock-core' to enable it (matching the pattern already
used by grails-datastore-core's build.gradle). Covers the new leaf-datastore
idempotent getDatastoreForConnection resolution and the new
routesUnqualifiedToMappedConnection() override.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…Spec closing PR #15780 coverage gap

Covers isValidParameter's GormProperties.IDENTITY shortcut (a method
parameter literally named `id` is always a valid identity parameter without
needing a matching declared property), via a save-style method binding an
explicit id alongside a real domain property.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ng PR #15780 coverage gap

grails-datamapping-tck is deliberately not test-configured (shared TCK base
class library, no test task of its own) - verified against a downstream
adapter's suite per this repo's own TCK constraints. Covers the new
@deprecated addAllDomainClasses(Collection) backward-compatible delegate to
registerDomainClasses(Class...).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…erSpec closing PR #15780 coverage gap

Covers the new GormValidatorAdapter.CASCADE_VALIDATION short-circuit guard
on isCascadable, which skips cascade validation entirely when explicitly
disabled regardless of the association/owning-side state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ing PR #15780 coverage gap

Covers the new explicit datastore/getDatastore()/setDatastore() Service
contract this PR added.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…pec closing PR #15780 coverage gap

Brand-new file. Covers all 3 createTransactionTemplate overloads, including
the (PlatformTransactionManager, TransactionAttribute) one GormStaticApi
doesn't call anywhere but is part of this factory's public contract.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ing PR #15780 coverage gap

Covers a method redundantly re-annotated with the same annotation as the
class it's in, exercising the new hasLocalAnnotation guard added to
AbstractMethodDecoratingTransformation to avoid re-decorating such methods.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
 coverage gap

Added testImplementation project(':grails-data-simple') to exercise the new
constructor-time persistentEntity/multiTenancyMode field assignments'
"not a MongoDatastore" fallback branch cheaply, without needing a real
Docker-backed MongoDB instance. The existing Docker-backed
MongoStaticApiMultiTenancySpec already covers the real-MongoDatastore
branch; together both ternary directions are now covered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@testlens-app

testlens-app Bot commented Jul 13, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 3a09015
▶️ Tests: 49436 executed
⚪️ Checks: 56/56 completed


Learn more about TestLens at testlens.app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants