Skip to content

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

Open
borinquenkid wants to merge 64 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 64 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 83.12883% with 330 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (feat/gorm-datastore-infra@7e50a21). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...oovy/org/grails/datastore/gorm/GormRegistry.groovy 81.6156% 23 Missing and 43 partials ⚠️
...ovy/org/grails/datastore/gorm/GormStaticApi.groovy 81.4978% 19 Missing and 23 partials ⚠️
...y/org/grails/datastore/gorm/GormApiResolver.groovy 82.1622% 4 Missing and 29 partials ⚠️
...oovy/org/grails/datastore/gorm/GormEnhancer.groovy 68.8172% 13 Missing and 16 partials ⚠️
...g/grails/datastore/gorm/finders/DynamicFinder.java 22.2222% 26 Missing and 2 partials ⚠️
...org/grails/datastore/gorm/GormValidationApi.groovy 74.2424% 3 Missing and 14 partials ⚠️
...y/org/grails/datastore/gorm/GormInstanceApi.groovy 85.2632% 2 Missing and 12 partials ⚠️
...y/org/grails/datastore/gorm/AbstractGormApi.groovy 82.0896% 0 Missing and 12 partials ⚠️
...gorm/transactions/GrailsTransactionTemplate.groovy 28.5714% 6 Missing and 4 partials ⚠️
...ails/datastore/gorm/AbstractGormApiRegistry.groovy 85.5072% 4 Missing and 6 partials ⚠️
... and 19 more
Additional details and impacted files

Impacted file tree graph

@@                      Coverage Diff                       @@
##             feat/gorm-datastore-infra     #15780   +/-   ##
==============================================================
  Coverage                             ?   52.1714%           
  Complexity                           ?      18415           
==============================================================
  Files                                ?       2054           
  Lines                                ?      96622           
  Branches                             ?      16846           
==============================================================
  Hits                                 ?      50409           
  Misses                               ?      38823           
  Partials                             ?       7390           
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%> (ø)
...ils/datastore/gorm/mongo/api/MongoStaticApi.groovy 63.4146% <100.0000%> (ø)
...rc/main/groovy/grails/gorm/DetachedCriteria.groovy 82.2368% <100.0000%> (ø)
...rails/gorm/multitenancy/CurrentTenantHolder.groovy 100.0000% <100.0000%> (ø)
.../grails/datastore/gorm/AbstractDatastoreApi.groovy 100.0000% <100.0000%> (ø)
...grails/datastore/gorm/DefaultGormApiFactory.groovy 100.0000% <100.0000%> (ø)
.../grails/datastore/gorm/GormEnhancerRegistry.groovy 100.0000% <100.0000%> (ø)
...ails/datastore/gorm/GormInstanceApiRegistry.groovy 100.0000% <100.0000%> (ø)
...grails/datastore/gorm/GormStaticApiRegistry.groovy 100.0000% <100.0000%> (ø)
... and 51 more
🚀 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
borinquenkid force-pushed the feat/gorm-datastore-infra branch from 9145c07 to 0f8e6c6 Compare July 4, 2026 22:25
borinquenkid added a commit that referenced this pull request Jul 4, 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
borinquenkid force-pushed the feat/gorm-registry-core-impl branch from e4773df to 3914546 Compare July 4, 2026 22:25
borinquenkid added a commit that referenced this pull request Jul 4, 2026
grails-data-neo4j is a separate Gradle build on an older baseline (Groovy 3.0.25 / Grails 6.0.0 /
javax) consuming published GORM. Document the two-PR path to wire it to the GormRegistry O(M+N) work:
PR1 migrates the build to the Groovy 4 / Java 21 / Jakarta baseline and onto core-impl's GORM
(8.0.0-SNAPSHOT); PR2 adds Neo4jGormApiFactory + registration and rewrites the entity traits from
GormEnhancer to GormRegistry. To be executed once #15780 CI confirms the GormRegistry SPI is stable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
borinquenkid added a commit that referenced this pull request Jul 4, 2026
grails-data-neo4j is a separate Gradle build on an older baseline (Groovy 3.0.25 / Grails 6.0.0 /
javax) consuming published GORM. Document the two-PR path to wire it to the GormRegistry O(M+N) work:
PR1 migrates the build to the Groovy 4 / Java 21 / Jakarta baseline and onto core-impl's GORM
(8.0.0-SNAPSHOT); PR2 adds Neo4jGormApiFactory + registration and rewrites the entity traits from
GormEnhancer to GormRegistry. To be executed once #15780 CI confirms the GormRegistry SPI is stable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
borinquenkid and others added 25 commits July 19, 2026 11:56
…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>
borinquenkid added a commit that referenced this pull request Jul 19, 2026
…hitecture

Responds to jdaugherty's CHANGES_REQUESTED review (#15779). Fixes the session/event
architecture concerns, narrows the public API surface, and splits out the unrelated
behavioral changes he flagged, per that review's blast-radius check against PR2/PR3
(#15780/#15790) - neither depends on anything reworked here beyond hasCurrentSession(),
which is now strictly more correct.

Session/event architecture (the core concern):
- SessionResolver/ThreadLocalSessionResolver no longer maintain independent ThreadLocal
  state. They're now a thin, stateless view over the same SessionHolder/TSM store
  DatastoreUtils already uses, so resolve() can never disagree with the transactional
  session. Nested scopes fall out for free from SessionHolder's existing stack.
- DatastoreUtils.doGetSession() no longer short-circuits through the resolver before
  transaction-synchronization registration and session validation - that shortcut
  silently bypassed both, plus the allowCreate contract.
- AbstractDatastore.hasCurrentSession() collapses to a single check now that resolver
  and TSM read the same state instead of being OR'd together.
- Dropped the unused, asymmetric resolve(String)/bind(String, S) qualifier surface from
  SessionResolver (zero callers anywhere in the codebase; the concrete class's own bind()
  admitted the feature was never finished).
- Replaced the hand-rolled event publisher with one composing SimpleApplicationEventMulticaster.
  addApplicationListener() now routes through getApplicationEventPublisher() (virtual) instead
  of the raw field, so it reaches whatever publisher a subclass (Mongo/Hibernate/Neo4j) actually
  publishes through, without touching those modules.
- Fixed the applicationEventPublisher triple-assignment and the bug where
  setApplicationContext(null) discarded a caller-installed custom publisher.
- @PreDestroy now closes every session held by the current thread's SessionHolder instead of
  just dropping the reference.

API surface:
- Datastore.getSessionResolver() is now a default method (was abstract - broke every external
  implementer); the default is now safe to construct per-call since the resolver holds no
  private state of its own.
- MappingContext.initialize(ConnectionSourceSettings) is back to protected on AbstractMappingContext,
  not promoted onto the public interface - nothing needed the promotion.

Restored/fixed semantics:
- DatastoreUtils.bindSession()/bindSession(creator) fail fast again (IllegalStateException) on a
  double-bind, instead of silently stacking - bindNewSession() already provides stacking for
  callers that need it (used internally by executeWithNewSession).
- CustomizableRollbackTransactionAttribute's copy constructors now deep-copy the rollback-rule
  list instead of aliasing the source's mutable list, and also copy transaction labels.
- AbstractConnectionSourceFactory.createSettings() now composes the same fallback-settings path
  create(name, configuration) uses, so it also applies the injected TenantResolver/customTypes.
- Deduplicated DatastoreUtils.executeWithNewSession's void-overload to delegate instead of
  copy-pasting the whole method body.

Split out (unrelated to SessionResolver infrastructure, reverted from this PR):
- KeyValueMappingContext's JpaMappingConfigurationStrategy -> GormMappingConfigurationStrategy
  swap - untested, no registry-related justification found.
- DirtyCheckingSupport's O(elements)/transitive dirty-checking change - algorithmic and semantic
  change, zero tests.
- AstUtils's annotation-copy dedup change - unrelated AST behavior change, no coverage.
- Dropped MappingContext.setMultiTenancyMode and ClassUtils.getIntegerFromMap - zero callers
  anywhere in the codebase.

Every touched class has new or updated Spock coverage, including the specific gaps the review
called out as untested: transaction precedence (resolver reads the same store as TSM), nested-session
restoration, concrete-datastore publisher wiring, and @PreDestroy cleanup. Full test sweep across
grails-datastore-core, grails-datamapping-core, grails-data-mongodb-core, grails-data-simple,
grails-data-hibernate5-core, and grails-data-hibernate7-core: BUILD SUCCESSFUL, 0 failures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@borinquenkid
borinquenkid force-pushed the feat/gorm-datastore-infra branch from ce346f0 to 7e50a21 Compare July 19, 2026 20:42
@borinquenkid
borinquenkid force-pushed the feat/gorm-registry-core-impl branch from 3a09015 to e049502 Compare July 19, 2026 20:42
borinquenkid added a commit that referenced this pull request Jul 19, 2026
grails-data-neo4j is a separate Gradle build on an older baseline (Groovy 3.0.25 / Grails 6.0.0 /
javax) consuming published GORM. Document the two-PR path to wire it to the GormRegistry O(M+N) work:
PR1 migrates the build to the Groovy 4 / Java 21 / Jakarta baseline and onto core-impl's GORM
(8.0.0-SNAPSHOT); PR2 adds Neo4jGormApiFactory + registration and rewrites the entity traits from
GormEnhancer to GormRegistry. To be executed once #15780 CI confirms the GormRegistry SPI is stable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@testlens-app

testlens-app Bot commented Jul 19, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: e049502
▶️ Tests: 40435 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