(2/3) feat: GORM O(M+N) scaling — GormRegistry, GormEnhancer, and core API refactor#15780
(2/3) feat: GORM O(M+N) scaling — GormRegistry, GormEnhancer, and core API refactor#15780borinquenkid wants to merge 64 commits into
Conversation
da5001a to
4b9d006
Compare
24dd795 to
b49c19f
Compare
|
Note on The Rationale: 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. |
|
@borinquenkid does #15771 fully replace this PR? And then does that remove the need for #15790 |
|
@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. |
There was a problem hiding this comment.
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.printlndebug 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.printlndebug output increateSchema. 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.
- 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>
9145c07 to
0f8e6c6
Compare
- 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>
e4773df to
3914546
Compare
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>
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>
…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>
…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>
ce346f0 to
7e50a21
Compare
3a09015 to
e049502
Compare
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>
✅ All tests passed ✅🏷️ Commit: e049502 Learn more about TestLens at testlens.app. |
Summary
Introduces the
GormRegistrysingleton that replaces the O(M×N) static map allocation inGormEnhancer. APIs are registered once at entity-registration time and looked up in O(1).GormRegistry— singleton keyed by(entityClass, qualifier); handlesMultiTenantqualifier expansion, thread-local preferred datastore, and concurrent-safe removal onclose()GormApiFactory/DefaultGormApiFactory— pluggable factory per datastore type; adapters override this to supply typed API instancesGormApiResolver— routes static/instance/validation API lookups through the registry with fallback to the default datastoreConnectionSourceNameResolver— extracts and normalises connection-source names from a datastore without leakingConnectionSourcesSupportinternalsGormEnhancer— delegates all registration and lookup toGormRegistry;allQualifiers()used only for datastore routing, not eager API allocationGormStaticApi/GormInstanceApi/GormValidationApi— useDatastoreResolverinstead of holding a directDatastorereference; support qualifier-aware execution viaexecuteQualified()AbstractGormApi.execute()— distinguishes datasource connection qualifiers from tenant-ID qualifiers to avoid overwriting the active tenant contextCurrentTenantHolder— thread-safe tenant binding forDISCRIMINATORmulti-tenancyServiceTransformation/TransactionalTransform— resolve transaction manager viaGormRegistryinstead of static map lookupsDefaultTransactionTemplateFactory/TransactionTemplateFactory— pluggable transaction template creation per datastore typeTest plan
./gradlew :grails-datamapping-core:testpasses (tests are in the companion PR)GormEnhancerAllQualifiersSpecStack
feat/gorm-datastore-infra)feat/gorm-registry-core-tests— full test suite for this PR🤖 Generated with Claude Code