feat: GORM O(M+N) scaling — GormRegistry, GormEnhancer, and core API refactor#15780
Open
borinquenkid wants to merge 13 commits into
Open
feat: GORM O(M+N) scaling — GormRegistry, GormEnhancer, and core API refactor#15780borinquenkid wants to merge 13 commits into
borinquenkid wants to merge 13 commits into
Conversation
This was referenced Jun 27, 2026
da5001a to
4b9d006
Compare
04d8d8f to
24dd795
Compare
…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>
…affected tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…k lines in HibernateGormEnhancer Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
24dd795 to
b49c19f
Compare
…mpatibility Adapter subclasses (SimpleMapDatastore, HibernateGormEnhancer, etc.) override getStaticApi/getInstanceApi/getValidationApi/createDynamicFinders as protected extension points. Removing them in the core refactor breaks compilation of those adapters until their own PRs are merged. Restore as @deprecated stubs delegating to GormRegistry so the adapter modules compile against this PR in isolation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adapter modules (hibernate5, converters, graphql) reference static methods and constructors removed in the core refactor. Restore them as @deprecated stubs delegating to GormRegistry so all adapters compile against this PR in isolation, without requiring the full stack to be merged together: - GormEnhancer: add 2-arg (Datastore, TxManager) constructor; static findStaticApi, findInstanceApi, findValidationApi, findDatastore delegates - GormStaticApi: add (Datastore, finders) and (Datastore, finders, TxManager) deprecated constructors extracting MappingContext from the Datastore - AbstractGormApi: restore deprecated persistentEntity field populated in both constructor paths so @CompileStatic subclasses (AbstractHibernateGorm*) can still read it directly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ter compat HibernateGormStaticApi (H7) assigns this.datastore = datastore in its constructor, requiring a setDatastore() setter. AbstractDatastoreApi now provides a deprecated setter that swaps the resolver to a StaticDatastoreResolver. Also fix CodeNarc MissingBlankLineBeforeAnnotatedField for persistentEntity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…spatch Three targeted fixes that eliminate H5/H7 runtime NPEs introduced by the GormRegistry refactor: 1. Remove setDatastore(Datastore) from AbstractDatastoreApi — adding a public setter for 'datastore' caused Groovy @CompileStatic to route constructor field assignments (e.g. this.datastore = hds in AbstractHibernateGorm- ValidationApi) through the setter instead of the declared local field, leaving that field null and causing ValidationEvent.<init> to throw IllegalArgumentException: null source. 2. Fix deprecated GormStaticApi(Class, Datastore, List[, PlatformTransactionManager]) constructors to wire a real DatastoreResolver closure instead of null, so getDatastore() returns the correct Datastore at runtime for H5/H7 adapters that still call these constructors. 3. Fix GormEnhancer.addStaticMethods mc.static.propertyMissing to convert any non-MissingPropertyException (e.g. ConfigurationException from H5's HibernateGormStaticApi.propertyMissing treating the name as a datasource qualifier) into MissingPropertyException so Groovy can fall through to methodMissing for dynamic finder dispatch (e.g. Person.countByTitle). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cApi Without setDatastore(Datastore) in AbstractDatastoreApi, getDatastore() makes 'datastore' a read-only property for classes without a local datastore field. HibernateGormStaticApi had no local datastore field, so this.datastore = datastore failed @CompileStatic compilation. The assignment was already redundant — the deprecated super constructor wires a DatastoreResolver that returns the correct HibernateDatastore. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…no GormApiFactory registered Without a specialized GormApiFactory registered for MongoDB (only H5/H7 register HibernateGormApiFactory via registerConstraints), GormRegistry.registerEntity fell back to DefaultGormApiFactory, which created base GormStaticApi instead of MongoStaticApi — causing ClassCastException in MongoEntity.currentMongoStaticApi(). When no specialized factory is registered for the datastore, delegate to the enhancer's overridden getStaticApi/getInstanceApi/getValidationApi methods, which polymorphically dispatch to adapter-specific anonymous subclass overrides (e.g. MongoDatastore's anonymous MongoGormEnhancer override that creates MongoStaticApi). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🚨 TestLens detected 141 failed tests 🚨Here is what you can do:
Test SummaryCI / Build Grails-Core (macos-latest, 21) > :grails-data-graphql-core:test
CI / Build Grails-Core (macos-latest, 21) > :grails-datamapping-core:test
CI / Build Grails-Core (macos-latest, 21) > :grails-fields:test
CI / Build Grails-Core (ubuntu-latest, 21) > :grails-data-graphql-core:test
CI / Build Grails-Core (ubuntu-latest, 21) > :grails-datamapping-core:test
CI / Build Grails-Core (ubuntu-latest, 21) > :grails-fields:test
CI / Build Grails-Core (ubuntu-latest, 25) > :grails-data-graphql-core:test
CI / Build Grails-Core (ubuntu-latest, 25) > :grails-datamapping-core:test
CI / Build Grails-Core (windows-latest, 25) > :grails-data-graphql-core:test
CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21) > :grails-data-graphql-core:test
🏷️ Commit: 7dcf26e Test Failures (first 10 of 141)CompileStaticServiceInjectionSpec > test @CompileStatic abstract class with injected @service properties compiles (:grails-datamapping-core:test in CI / Build Grails-Core (macos-latest, 21))CompileStaticServiceInjectionSpec > test impl has datastore infrastructure when abstract class has @service properties (:grails-datamapping-core:test in CI / Build Grails-Core (macos-latest, 21))DefaultInputRenderingPersistentSpec > input for a #description property does have `.id` at the end of the name > input for a many-to-one property does have `.id` at the end of the name (:grails-fields:test in CI / Build Grails-Core (macos-latest, 21))DefaultInputRenderingPersistentSpec > input for a #description property does have `.id` at the end of the name > input for a one-to-one property does have `.id` at the end of the name (:grails-fields:test in CI / Build Grails-Core (macos-latest, 21))DefaultInputRenderingPersistentSpec > input for a #description property doesn't have `.id` at the end of the name > input for a many-to-many property doesn't have `.id` at the end of the name (:grails-fields:test in CI / Build Grails-Core (macos-latest, 21))DefaultInputRenderingPersistentSpec > input for a #description property is a select > input for a many-to-many property is a select (:grails-fields:test in CI / Build Grails-Core (macos-latest, 21))DefaultInputRenderingPersistentSpec > input for a #description property is a select > input for a many-to-one property is a select (:grails-fields:test in CI / Build Grails-Core (macos-latest, 21))DefaultInputRenderingPersistentSpec > input for a #description property is a select > input for a one-to-one property is a select (:grails-fields:test in CI / Build Grails-Core (macos-latest, 21))DefaultInputRenderingPersistentSpec > select for a #description property with a value of #value has the correct option selected > select for a many-to-many property with a value of [Homer Simpson] has the correct option selected (:grails-fields:test in CI / Build Grails-Core (macos-latest, 21))DefaultInputRenderingPersistentSpec > select for a #description property with a value of #value has the correct option selected > select for a many-to-one property with a value of Homer Simpson has the correct option selected (:grails-fields:test in CI / Build Grails-Core (macos-latest, 21))Muted Tests (first 20 of 141)Select tests to mute in this pull request:
Reuse successful test results:
Click the checkbox to trigger a rerun:
Learn more about TestLens at testlens.app. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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