The GitHub Actions job "SiteMesh 2 Compatibility" on grails-core.git/8.0.x has 
succeeded.
Run started by GitHub user borinquenkid (triggered by borinquenkid).

Head commit for run:
2926a0f6446bfcbe2b9809c83f997cf2ae1e739d / Walter B Duque de Estrada 
<[email protected]>
feat: GORM O(M+N) scaling — GormRegistry, SessionResolver infrastructure, and 
core-class tests (consolidates #15779, #15780, #15790) (#16066)

* fix: capture STACK_LOG output via a Logback appender instead of System.err

DefaultStackTraceFilterer.STACK_LOG routes through commons-logging, which
resolves to a jcl-over-slf4j binding on this classpath -- so its output never
touches System.err, regardless of test ordering or timing. Swapping
System.err therefore never observes the emitted message, making
GrailsUtilStackFiltererSpec and GrailsBootstrapRegistryInitializerSpec fail
deterministically. Attach a ListAppender directly to the public STACK_LOG_NAME
logger instead, which is unaffected by which commons-logging backend wins the
classpath.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: convert GrailsExceptionResolverSpec to LogCapture, remove last 
System.err capture

Address jamesfredley's review on #16067: this was the one remaining latent
instance of the same fragility -- three sites asserting on rendered
System.err output for the STACK_LOG and GrailsExceptionResolver loggers,
including one match on the literal console layout string 'ERROR StackTrace '.
It passed only because grails-web-mvc's test classpath happened to carry
slf4j-simple, whose SYS_ERR output choice re-reads System.err per call rather
than caching it -- the same trap that broke these tests in grails-core once
that module got a deterministic logback-test.xml.

Swap the module's test logging binding from slf4j-simple to grails-core's
test fixtures (which bring logback-classic transitively), and rewrite the
three specs to assert on captured ILoggingEvents instead of console text --
including inspecting each event's throwableProxy stack frames directly
rather than counting substrings in rendered output.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* feat: add SessionResolver infrastructure and extend core datastore APIs

Introduce SessionResolver and ThreadLocalSessionResolver for thread-safe
session lookup without coupling callers to a specific Datastore instance.
Extend AbstractDatastore, Datastore, DatastoreUtils, and MappingContext
with the hooks GormRegistry needs for O(M+N) API registration.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: restore concrete Service trait methods to fix @CompileStatic compilation

Making getDatastore()/setDatastore() abstract in the Service trait breaks
@CompileStatic classes that implement the trait (e.g. DefaultTenantService),
because Groovy's static compiler does not properly satisfy trait abstract
method contracts when the implementing class declares the same method in its
own body. Restore the original backing-field implementation.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: widen MongoMappingContext.initialize visibility to public

MappingContext interface declares initialize(ConnectionSourceSettings) as
public; MongoMappingContext.initialize was protected, which Java rejects
as assigning weaker access privileges to an interface method implementation.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: address Copilot review findings on PR #15779

- ThreadLocalSessionResolver: qualifier-bound sessions were stored in a
  single shared ConcurrentHashMap, leaking across threads; move to a
  ThreadLocal<Map> and clear it on unbind().
- AbstractDatastore.DefaultApplicationEventPublisher: dispatched every
  event to every listener regardless of declared generic type, risking
  ClassCastException for typed listeners; filter via Spring's own
  GenericApplicationListenerAdapter/ResolvableType before invoking, and
  make the listener list a CopyOnWriteArrayList.
- DatastoreUtils.bindSession: silently skipped binding when a
  SessionHolder was already present for the datastore, which could
  leave a freshly created session unbound (e.g. DataTestSetupInterceptor
  creates a new session per test method). Add the session to the
  existing holder instead, matching bindNewSession's push/pop semantics.
- Align new spec file license headers with the standard ASF header used
  elsewhere in the module.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: address review feedback on SessionResolver/AbstractDatastore architecture

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 <[email protected]>

* fix: guard getTenantId() DISCRIMINATOR mode and close review-flagged coverage 
gaps

getTenantId()'s lazy fallback ignored the DISCRIMINATOR-mode check that
initialize() uses and NPE'd when persistentProperties was null under
deferred entity initialization. Also adds unit coverage for the
still-live gaps Codecov flagged after review: Datastore's default
getSessionResolver(), AbstractDatastore's reflective listener fallback,
its Object-payload event wrapping and destroy() error handling, the
bare-TransactionDefinition copy constructor, and the 3-arg
ConnectionSourceSettingsBuilder constructor.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: address jdaugherty's follow-up review on 
SessionResolver/AbstractDatastore

Three remaining issues from the re-review of #15779:

- ThreadLocalSessionResolver.unbind() called unbindResourceIfPossible(),
  discarding the whole SessionHolder instead of popping just the top
  session. bind(A); bind(B); unbind() lost A entirely instead of
  restoring it, and neither session was closed. unbind() now pops and
  closes only the top session via the same
  removeSession()/isEmpty()/closeSessionOrRegisterDeferredClose() path
  DatastoreUtils.executeWithNewSession already uses, leaving the outer
  binding intact. The nested-scope test previously asserted the
  destructive behavior as correct; it now asserts restoration.

- AbstractDatastore.hasCurrentSession() read the swappable sessionResolver
  field, while getCurrentSession() read TSM/SessionHolder directly via
  DatastoreUtils.doGetSession() - a caller-installed custom resolver could
  make these two methods disagree. setSessionResolver() had zero callers
  anywhere in the codebase (confirmed via search), so removed it and made
  sessionResolver final: both methods are now guaranteed to read the same
  authoritative state.

- addApplicationListener()'s reflective fallback silently logged and
  swallowed registration failures for a plain ApplicationEventPublisher
  with no addApplicationListener method, so a caller had no way to know
  the listener would never fire. It now throws IllegalStateException
  instead of silently succeeding from the caller's perspective.

Every fix has updated Spock coverage. Full grails-datastore-core,
grails-datamapping-core, and grails-data-simple suites pass; codeStyle/
CodeNarc clean.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* test(grails-datastore-core): close Codecov patch-coverage gap on 
getTenantId()'s lazy fallback

PR #15779's getTenantId() lazy DISCRIMINATOR-mode fallback (lines 101-108, 
added to fix
jdaugherty's review comment about the eager-only lookup) had 25% Codecov patch 
coverage - 5
missing lines and 1 partial branch - despite 
AbstractPersistentEntityGetTenantIdSpec already
existing. Root cause: that spec's existing tests all configure DISCRIMINATOR 
mode *before*
adding the entity, so initialize()'s eager loop (line 165-169) already assigns 
tenantId by the
time getTenantId() runs, and the new lazy block's `this.tenantId == null` guard 
is never true.

Added 4 tests that switch the context into DISCRIMINATOR mode *after* the 
entity is already
initialized (an entity added while still in NONE mode never runs the eager 
assignment, so
tenantId stays null even once DISCRIMINATOR mode is applied later) - the exact 
scenario the
lazy fallback exists for:
- successful lazy match (drives the loop's find-and-break path)
- no tenantId property present (drives the loop's exhaust-without-match path; 
this needed NONE
  mode at initialize() time since DISCRIMINATOR mode at that point makes 
initialize() itself
  throw ConfigurationException for a multi-tenant class with no tenant 
identifier property)
- a plain non-multi-tenant entity (drives the isMultiTenant()==false 
short-circuit branch of
  the compound guard, the one remaining uncovered branch outcome after the 
above)

Verified via local JaCoCo: lines 101-110 (the PR's new code) now have 0 missed 
instructions and
0 missed branches, up from 5 missing lines/1 partial branch. Full 
grails-datastore-core suite
and codeStyle both pass with no regressions.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* feat: GORM O(M+N) scaling — GormRegistry, GormEnhancer, and core API 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 <[email protected]>

* fix: restore registerConstraints to GormEnhancer dropped in scaling refactor

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 <[email protected]>

* fix: restore registerConstraints hook and fix MongoStaticApi fields dropped 
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 <[email protected]>

* fix: distinguish datasource connection qualifier from tenant ID in 
AbstractGormApi.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 <[email protected]>

* fix: mode-aware tenant lookup and bound DatastoreResolvers in GormRegistry

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 <[email protected]>

* fix: register HibernateGormApiFactory in registerConstraints; update affected 
tests

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* style: remove unused ConnectionSource import and fix consecutive blank lines 
in HibernateGormEnhancer

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: restore deprecated GormEnhancer extension points for backward 
compatibility

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 <[email protected]>

* fix: restore deprecated backward-compat API surface for adapter modules

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 <[email protected]>

* fix: restore deprecated setDatastore and persistentEntity for H7 adapter 
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 <[email protected]>

* fix: resolve null datastore in H5/H7 validation and dynamic finder dispatch

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 <[email protected]>

* fix: remove redundant this.datastore assignment in HibernateGormStaticApi

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 <[email protected]>

* Fix MongoDB functional tests: fall back to enhancer API factory when 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 <[email protected]>

* fix: restore null-tolerant service/datastore contracts in GormRegistry core

Repairs regressions introduced by this PR's GormRegistry O(M+N) refactor in
grails-datamapping-core. All four production fixes correct code this PR
introduced or rewrote:

- GormRegistry.registerEntity: always create the entity APIs via the
  GormApiFactory instead of falling back to the deprecated
  GormEnhancer.getStaticApi stub, which performs a registry lookup that
  returns null at registration time and left default-factory datastores with
  no registered APIs.
- GormEntity.staticPropertyMissing: resolve the static API directly and throw
  MissingPropertyException on null, rather than relying on catching an
  IllegalStateException from a dynamically dispatched currentGormStaticApi()
  call (which escaped the catch under invokedynamic). currentGormStaticApi /
  currentGormInstanceApi made private again to match the prior API surface.
- ServiceTransformation: generate getDatastore() to resolve via
  GormRegistry.getDatastore(domainClass), which returns null when GORM is not
  configured, restoring the null-tolerant contract the generated service
  infrastructure (validator factory, transaction manager) depends on.
- TransactionalTransform.hasTransactionalAnnotation: treat @NotTransactional
  as an explicit transactional decision so the read/write service
  implementers do not impose a default @ReadOnly/@Transactional, which
  otherwise wrapped @NotTransactional methods in a transaction template and
  forced transaction-manager resolution before method validation could run.

Tests updated to the new architecture: GormEnhancerAllQualifiersSpec rewritten
against GormRegistry; service specs adjusted for protected finder methods,
the removed datastore backing field on domain-targeted services, and the
trait-woven datastore accessors whose @Generated marker is managed by Groovy
trait weaving.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: clear GormRegistry core-impl regressions (H5 core 42→6)

Reconcile the structural O(M+N) GormRegistry rewrite (089ca4396f) against the
allocation/TCK specs, fixing 8 root causes uncovered while driving the 
Hibernate5
core suite from 42 failures to 6.

Core regressions reintroduced by the rewrite:
- GormInstanceApi.save: restore markDirty() so an explicit save() re-persists a
  clean/detached instance (ExplicitSaveRepersistsSpec).
- GormStaticApi.withDatastoreSession: run the GORM Session directly instead of
  delegating to withSession (which adapters override to the native session)
  (GormStaticApiWithDatastoreSessionSpec).
- GormStaticApi string-query overloads (executeQuery/executeUpdate/find/findAll
  over CharSequence): delegate convenience overloads to the terminal overload 
and
  restore the unsupported() helper, so adapter HQL overrides are reachable 
instead
  of throwing UnsupportedOperationException 
(GormStaticApiStringQueryDelegationSpec).
- GormStaticApi first/last: restore the default sort by the identity property 
when
  no sort is supplied.
- ListOrderByFinder: resolve the sort direction before applying order so an 
explicit
  order:'desc' argument is honored.

Allocation reconciliation:
- GormRegistry.registerEntity eagerly warms APIs for an entity's 
explicitly-mapped
  datasources (bounded M side); ALL/tenant qualifiers stay lazy (unbounded N 
side).
- AbstractGormApiRegistry.isAllocated(className, qualifier): introspection of 
whether
  an API is materialized without triggering lazy creation 
(GormEnhancerAllQualifiersSpec
  eager/lazy tests, GormApiAllocationSpec).
- GormApiAllocationSpec: verify per-tenant API identity via the tenant 
qualifier rather
  than a tenant-less DEFAULT lookup; strict-mode DEFAULT resolution 
intentionally still
  throws TenantNotFoundException (load-bearing safety).

Pre-existing bug surfaced (also present in feat/gorm-datastore-infra):
- HibernateSession.retrieveAll: drop the redundant outer 
criteriaBuilder.in(...) wrapper
  that emitted a malformed `id in (..) in ()` (QuerySyntaxException) for getAll.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: resolve multi-tenant dynamic finder dispatch and tenant session passing 
(H5 core 6→3)

Root cause A of the GormRegistry core-impl regressions: dynamic finders such as
findByName failed on multi-tenant entities with MissingMethodException("call").

The structural rewrite added mc.static.propertyMissing in GormEnhancer. When a
finder is invoked through a channel that resolves the name as a property before
falling back to a method call (Spock's verifyMethodCondition / InvokerHelper),
that property channel reached HibernateGormStaticApi.propertyMissing, whose
override returned a connection-qualifier API for ANY name — including finder
names. Groovy then invoked .call(arg) on the qualifier API, yielding
methodMissing("call"). The base GormStaticApi.propertyMissing is finder-aware
(returns a finder closure first) but the adapter overrides bypassed it.

Fix the H5 and H7 HibernateGormStaticApi.propertyMissing overrides to delegate
to super.propertyMissing, which resolves finder property access to a finder
closure before falling back to connection-source qualifier lookup. Remove the
now-unused GormEnhancer (H5) and ConnectionSourcesProvider (H7) imports.

Also fix a second bug masked by the first: the new early
childDatastore.getCurrentSession() branch in Tenants.withId passed the GORM
session to two-argument closures typed against the native Hibernate session in
DISCRIMINATOR mode. Guard that branch with !isSharedConnection() so shared
connection modes (DISCRIMINATOR, SCHEMA) use the proven shared-connection
withSession path; DATABASE mode keeps the session-reuse optimization.

Add MultiTenantFinderDispatchSpec covering both behaviors via public APIs.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: honor getAll id order, null slots, and id conversion in Hibernate 
retrieveAll (H5 core 3→0)

Root cause B of the GormRegistry core-impl regressions: getAll() did not 
implement
the documented GORM contract. HibernateSession.retrieveAll issued a single
`id IN (...)` query and returned rows in database order, so it could not 
preserve
the supplied id order, never produced a null slot for an unmatched id, and did 
not
convert String ids to the entity's identifier type.

Rework retrieveAll in both the Hibernate 5 and Hibernate 7 sessions to:
- convert each requested id to the entity identifier type via the mapping
  context's ConversionService (preserving order and duplicates),
- query only the distinct, non-null ids,
- index the results by identifier, then reassemble the result list in the
  requested order with a null slot for every id that resolved to no row.

Covered by the TCK GormEnhancerSpec getAll order/null-slot tests and
HibernateGetAllConvertibleIdSpec. Full grails-data-hibernate5-core suite is 
green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: register MongoGormApiFactory so Mongo entities resolve a MongoStaticApi

GormRegistry fell back to DefaultGormApiFactory (a base GormStaticApi) for
MongoDatastore, breaking the `(MongoStaticApi) GormEnhancer.findStaticApi(...)`
cast in MongoEntity. Add MongoGormApiFactory (extends DefaultGormApiFactory,
overrides only createStaticApi -> MongoStaticApi) and register it in
MongoGormEnhancer.registerConstraints, calling super to keep the
ConstraintRegistrar. Clears all MongoStaticApi ClassCastExceptions
(:grails-data-mongodb-core:test 73 -> 7 failures).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: apply pagination args before Query.list() in findAll-by-example

GormStaticApi.findAll(D example, Map args) called query.list(args), but
Query.list() takes no Map argument (only criteria builders expose list(Map)),
raising MissingMethodException on adapters whose Query has no list(Map) (e.g.
MongoQuery). Apply args via DynamicFinder.populateArgumentsForCriteria, then
call query.list() — mirroring list(Map). Add FindByExampleSpec to the in-memory
CoreTestSuite TCK selection so the behaviour is covered for free.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: implement find/findAll-by-example via findWhere/findAllWhere delegation

The rewrite's createQuery + populateQueryByExample path did not apply 
restrictions on
Hibernate 7 (its session yields no usable persister / entity-access for a 
*transient*
example), so example queries returned all rows. Route find/findAll(example) 
through a
property map built from the example's own getters, delegated to 
findWhere/findAllWhere —
which every adapter already implements correctly (H5/H7 override; in-memory 
uses core),
and which guard an empty map to null. Restores the proven upstream approach and 
removes
the datastore-coupled populateQueryByExample helper. Covered by TCK 
FindByExampleSpec.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: H7 first/last(Map) inherit core's order-aware implementation

H7's HibernateGormStaticApi overrode first/last(Map) as 
list(m).first()/.last(), which
ignored sort direction (last returned the first element for an explicit sort). 
Remove the
overrides so H7 inherits core GormStaticApi.first/last(Map), which apply max:1 
+ order
asc/desc + a default identity sort. Covered by TCK FirstAndLastMethodSpec 
sort-parameter.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* test: align H7 core tests with the GormRegistry rewrite

- GormApiAllocationSpec: read allocation via 
GormRegistry.*ApiRegistry.isAllocated (the
  GormEnhancer.STATIC_APIS/INSTANCE_APIS/VALIDATION_APIS maps were removed by 
the rewrite);
  for strict-mode (DATABASE/SCHEMA) entities verify per-tenant allocation via 
the tenant
  qualifier instead of a tenant-less DEFAULT lookup, which correctly throws
  TenantNotFoundException.
- GrailsIdentityGeneratorSpec: import the TCK TestEntity/ChildEntity it 
registers.
- FirstAndLastMethodSpec (TCK): extend the composite-key @PendingFeatureIf to 
the H7 suite,
  matching H5 (the feature was previously @Ignore).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: H7 disjunction/conjunction/negation operate on detachedCriteria

HibernateQuery builds its JPA criteria from `detachedCriteria`, and overrides 
add/or/and/not
to write there — but inherited the core disjunction()/conjunction()/negation() 
factory
methods, which add to the unused base Query `criteria` field. A junction 
created via those
factories (e.g. CountByFinder's `q.disjunction()` for countByXOrY) was 
therefore silently
dropped, so count-over-OR ignored the disjunction and returned the total count 
(countByXAndY,
built via q.add(), was unaffected). Override the three factories to add to 
detachedCriteria.
Clears GormEnhancerSpec "count by query" and FindByMethodSpec OR-multiple; 
grails-data-
hibernate7-core is now fully green (21 -> 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: MultiTenant.eachTenant must not force current-tenant resolution

eachTenant enumerates tenants, so it has no current tenant — but it fetched its 
api via
GormRegistry.findStaticApi, whose strict-mode (SCHEMA/DATABASE) path resolves 
the current
tenant and throws TenantNotFoundException when none is bound. Use the 
non-resolving
getStaticApi to obtain the base api, leaving the load-bearing strict-mode throw 
intact for
actual queries. Fixes Mongo SingleTenancySpec/MongoConnectionSourcesSpec 
tenancy tests whose
setup calls eachTenant with no tenant bound; H5/H7 tenancy specs remain green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* test: isolate Mongo multi-tenancy specs from singleton GormRegistry pollution

SchemaBasedMultiTenancySpec passed in isolation but failed in the full suite: a 
prior
DATABASE-mode tenancy spec left a stale CompanyB->datastore binding in the 
singleton
GormRegistry, so this SCHEMA-mode spec resolved the wrong datastore 
(CompanyB.DB.name
'test1Db' instead of 'test1'). Match the MongoDB adapter PR (#15783): make the 
datastore
@Shared, create it once in setupSpec after GormRegistry.reset() to clear leaked 
state, and
keep per-feature tenant-property clearing in setup(). Applied to 
SchemaBasedMultiTenancySpec
and MultiTenancySpec. Mongo core 3 -> 1 (only the count-OR disjunction remains).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: @CurrentTenant data services resolve the primary datastore for tenant 
management

Regression from the GormRegistry rewrite (verified against the pre-rewrite 
base, where the
schema-per-tenant example passed): the @CurrentTenant AST transform located the 
TenantService via
apiResolver.findDatastore(domainClass). For a multi-tenant entity with a 
resolvable current tenant,
findDatastore correctly recurses into the current tenant's per-connection CHILD 
datastore (right for
tenant-scoped queries) — but that child cannot see schemas added at runtime via 
addTenantForSchema
(registered on the primary), so Tenants.withId -> withNewSession(tenantId) 
threw "DataSource not found".

The TenantService must be resolved on the tenant-MANAGER (primary) datastore, 
as it was pre-rewrite
(service's $targetDatastore). Add GormApiResolver.findServiceDatastore(entity) 
— a non-tenant-resolving
lookup of the entity's primary (DEFAULT) datastore — and use it in 
TenantTransform's service branch.
findDatastore is unchanged (tenant query routing preserved). The non-service 
path (findSingleDatastore,
null entity) already skips tenant resolution. Restores the schema-per-tenant 
example; H7 core stays green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* test: @PendingFeature the pre-existing Mongo count-over-OR disjunction bug

DisjunctionQuerySpec "Count all dogs or pets with the name Jack" 
(countByTypeOrName == 3 returns 1)
is a pre-existing MongoDB defect: count over an OR disjunction under-counts, 
while find-over-OR works
and the MongoQuery count path is unchanged from before the GormRegistry 
rewrite. Mark it @PendingFeature
(it already carried @Issue('GPMONGODB-380')) so it does not block this PR, and 
track the independent fix
in https://github.com/apache/grails-core/issues/15789. grails-data-mongodb-core 
now reports 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: resolve GORM regressions surfaced by the registry refactor

Fixes the functional/CI failures introduced by the O(M+N) GormRegistry rework,
verified locally across unit, TCK, container (Mongo) and functional suites.

Datastore routing (SimpleMapDatastore, GormRegistry, GormEnhancer,
MultipleConnectionSourceCapableDatastore):
- An entity mapped only to non-default connections routes its unqualified
  operations per datastore type. Real datastores (Hibernate, Mongo) manage a
  session per connection, so DEFAULT goes to the first mapped connection. The
  in-memory mock used by unit tests manages a single session on the parent and
  only fabricates isolated children for explicit connection access; routing its
  unqualified operations to such a child dropped writes the harness never
  flushes. A new MultipleConnectionSourceCapableDatastore
  .routesUnqualifiedToMappedConnection() (default true; the interface is now
  Java so the default method is visible to the Java datastore implementations)
  drives this, and GormRegistry's DEFAULT fallback consults it. This keeps
  CarSpec (datasource-mapped entity, single-store mock), MultipleDataSourceSpec
  (isolated datasources) and the Hibernate multi-datasource guards all correct.
- GormEnhancer.allQualifiers resolves the datastore's connection sources freshly
  instead of from a list cached at construction, so schema-/database-per-tenant
  tenants added at runtime via addTenantForSchema are mapped.
- SimpleMapDatastore.getDatastoreForConnection resolves a leaf/per-tenant 
child's
  own connection name to itself, keeping nested Tenants.withId resolution
  idempotent (mirrors ChildHibernateDatastore).

Hibernate dirty marking (HibernateGormApiFactory, H5 + H7):
- The factory took markDirty from the enhancer (default true) instead of the
  datastore (SETTING_MARK_DIRTY, default false). With it forced true, save()
  marked clean attached entities dirty, flushing a full UPDATE and firing
  spurious beforeUpdate/afterUpdate events. Now sourced from the datastore;
  GormApiAllocationSpec guards it.

Dynamic finders (ListOrderByFinder):
- Restore decapitalizeFirstChar (JavaBeans decapitalize left "ISize" unchanged),
  so listOrderBy works for Hungarian-notation properties such as "iSize".

Tests (graphql integration specs):
- The query-count assertions counted every stdout line; an emitted Hibernate 5
  legacy-criteria deprecation warning now lands in the capture window. They 
count
  only "Hibernate:" SQL lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* chore: strip scope-creep config noise from the GormRegistry PR

Remove changes that rode along in the squashed scaling commit but are
unrelated to the GORM O(M+N) / GormRegistry work, restoring these files
to their base versions:

- .gitignore: drop personal AI-tooling ignores (.junie/, .cursorrules, etc.)
- build.gradle: drop repo-wide test logging-level overrides
- gradle/rat-root-config.gradle: drop ISSUES.md RAT exclusion (no such file)
- gradle/{hibernate5,hibernate7,mongodb}-test-config.gradle: drop blank-line 
churn

No source or behavior change; keeps the PR diff reviewable.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: address Copilot review findings on PR #15780

- 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 <[email protected]>

* test: verify @Service+@CurrentTenant routing in DATABASE multi-tenancy mode

Added while spot-checking the abandoned worktree/fix-tenant-routing branch
for cleanup: that branch's AbstractGormApi.groovy change (a DATABASE-mode
direct-execute path + removing a CurrentTenantHolder DEFAULT-qualifier
check) predates the current isConnectionQualifier-based routing and
findServiceDatastore fix already in this codebase. This test confirms the
current mechanism already handles the scenario correctly (each tenant's
@Service call routes and isolates data properly in DATABASE mode), so the
old branch is fully superseded rather than containing an unported fix.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: restore copyAnnotations dedup guard needed by ServiceTransformation

PR1 (#15779) correctly reverted this AstUtils.copyAnnotations change as
unrelated scope creep on the SessionResolver PR, per jdaugherty's review.
But merging that revert into this branch broke ServiceTransformSpec and
MethodValidationTransformSpec: ServiceTransformation.groovy pre-annotates
generated method impls with @NotTransactional/@ReadOnly before calling
copyAnnotations(method, methodImpl), and without the dedup guard, Groovy
rejects the resulting duplicate annotation outright.

This is where the fix actually belongs - it has real, previously-missing
test coverage now (AstUtilsSpec) plus the existing integration coverage
in ServiceTransformSpec/MethodValidationTransformSpec that demonstrably
fails without it.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* test(grails-datamapping-core): add GormApiResolverSpec closing PR #15780 
coverage gap

GormApiResolver and its 4 datastore-selector helper classes (Preferred/
Qualified/ActiveSession/Default) had only 48.1% patch coverage - almost
all multi-tenancy, preferred-datastore, and qualified-connection
resolution logic was untested, only exercised incidentally via the
plain single-datastore path used elsewhere in the suite.

56 tests exercise each selector directly (they take GormRegistry/
GormEnhancerRegistry as plain params, both usable via their implicit
no-arg constructor for full per-test isolation instead of the shared
singleton), plus GormApiResolver's own public methods. Local JaCoCo:
fully-missing lines 61 -> 5; remaining gaps are branch-level partials
on already-executed lines. Full module suite green, 0 regressions,
codeStyle clean.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix(grails-datamapping-core): restore saveAll's persisted-id return value; 
add GormStaticApiSpec

saveAll(Iterable) discarded session.persist()'s return value (the
persisted ids) and returned session.flush()'s result instead - flush()
is void, so saveAll always returned null. This was introduced when the
GormRegistry rewrite added the flush() call; the pre-rewrite
implementation returned persist()'s result directly with no flush.
Fixed by capturing persist()'s ids explicitly and returning them after
the flush.

GormStaticApiSpec (46 tests) exercises the constructors, transaction-
manager/qualified-execution/persistent-entity resolution helpers,
method/propertyMissing dynamic-finder and connection-qualifier
dispatch, the full CRUD/criteria/query surface (via a real
SimpleMapDatastore rather than hand-mocked sessions), transaction and
tenant delegation. Whole-file line coverage 82.2% (241/293), 0
regressions in the full module suite, codeStyle clean. lock(Serializable)
and mutex() are left uncovered - SimpleMapDatastore's session doesn't
support real locking and faking it would test nothing real.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* test(grails-datamapping-core): add GormRegistryCoverageSpec closing PR #15780 
coverage gap

GormRegistry.groovy sat at 75.5% patch coverage (88 missing/partial
lines) - the static entry-point delegators, string-keyed API lookups,
and the multi-tenancy priority-resolution logic (identical across
resolveStaticApi/resolveInstanceApi/resolveValidationApi) were largely
untested, only exercised incidentally by whatever the pre-existing
GormRegistrySpec's datastore-registration tests happened to touch.

23 tests exercise these directly against fresh GormRegistry instances
(matching item 1/2's isolation pattern), including a corrected
understanding of AbstractGormApiRegistry's lazy qualification: there is
no 3-arg register(class, qualifier, api) - a non-default-qualifier api
is derived on demand via qualify(), which re-resolves the datastore
for that qualifier and only materializes a new api if it differs from
the default api's own datastore. Tests that exercise this use a real
SimpleMapDatastore for the entity's mapping context (qualify's
re-resolution needs a real PersistentEntity lookup) alongside Stub
datastores for the multi-tenancy-specific behavior.

Line coverage 90.5% (325/359, up from 77.2%), only 1 of 87 methods now
fully uncovered (down from 17). Full module suite green, 0 regressions,
codeStyle clean (no production code touched this time).

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* test(grails-datamapping-core): add GormEnhancerCoverageSpec closing PR #15780 
coverage gap

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

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

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* test(grails-datamapping-core): add GormValidationApiCoverageSpec closing PR 
#15780 coverage gap

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

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

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* test(grails-datamapping-core): add GormInstanceApiSpec closing PR #15780 
coverage gap

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

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

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* test(grails-datamapping-core): add AbstractGormApiSpec closing PR #15780 
coverage gap

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

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

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* test(grails-datamapping-core): add TenantsSpec closing PR #15780 coverage 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 <[email protected]>

* test(grails-datamapping-core): add DynamicFinderCoverageSpec closing PR 
#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 <[email protected]>

* test(grails-datamapping-core): add ServiceTransformationCoverageSpec 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 <[email protected]>

* test(grails-datamapping-core): add MultiTenantEventListenerSpec closing 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 <[email protected]>

* test(grails-datamapping-core): add AbstractStringQueryImplementerCoverageSpec 
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 <[email protected]>

* test(grails-datamapping-core): add DefaultTenantServiceSpec closing PR #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 <[email protected]>

* test(grails-datamapping-core): add CurrentTenantHolderSpec closing PR #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 <[email protected]>

* test(grails-datamapping-core): add TenantDelegatingGormOperationsSpec 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 <[email protected]>

* test(grails-datamapping-core): add GrailsTransactionTemplateSpec closing 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 <[email protected]>

* fix(grails-datamapping-core): guard CriteriaBuilder's cache/join/select 
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 <[email protected]>

* test(grails-datamapping-core): add GormStaticApiRegistrySpec closing PR 
#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 <[email protected]>

* test(grails-datamapping-core): add ConnectionSourceNameResolverSpec closing 
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 <[email protected]>

* test(grails-datamapping-core): add GormValidationApiRegistrySpec and 
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 <[email protected]>

* test(grails-datamapping-core): add AbstractFinderSpec closing PR #15780 
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 <[email protected]>

* test(grails-datamapping-core): add FindAllByImplementerCoverageSpec closing 
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 <[email protected]>

* test(grails-datamapping-core): add FindOrSaveByFinderSpec closing PR #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 <[email protected]>

* test(grails-datamapping-core): add AbstractDatastoreApiSpec closing PR #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 <[email protected]>

* test(grails-data-simple): add SimpleMapDatastoreSpec closing PR #15780 
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 <[email protected]>

* test(grails-datamapping-core): add AbstractServiceImplementerCoverageSpec 
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 <[email protected]>

* test(grails-data-hibernate5-core): add GrailsDataTckManagerSpec closing 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 <[email protected]>

* test(grails-datamapping-core): extend MappingContextTraversableResolverSpec 
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 <[email protected]>

* test(grails-datamapping-core): add DefaultTransactionServiceSpec closing PR 
#15780 coverage gap

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* test(grails-datamapping-core): add DefaultTransactionTemplateFactorySpec 
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 <[email protected]>

* test(grails-datamapping-core): extend TransactionalTransformSpec closing 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 <[email protected]>

* test(grails-data-mongodb-core): add MongoStaticApiSpec closing PR #15780 
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-Mon…

Report URL: https://github.com/apache/grails-core/actions/runs/32297135623

With regards,
GitHub Actions via GitBox

Reply via email to