Skip to content

Hibernate 7: give criterion handlers the query context and let a handler order by an expression - #16696

Merged
borinquenkid merged 41 commits into
refactor/java-to-groovy-grails-data-hibernate7from
feat/criteria-handler-context
Oct 25, 2026
Merged

borinquenkid merged 41 commits into
refactor/java-to-groovy-grails-data-hibernate7from
feat/criteria-handler-context

Conversation

@borinquenkid

@borinquenkid borinquenkid commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Fixes #16563.

Two additive extension points for the Hibernate 7 criteria builder, asked for by grails-postgresql-extensions' Hibernate 7 port (gpc/grails-postgresql-extensions#136), which today has to guess which join GORM made for an alias and smuggle an order by through a criterion.

Resolving property paths in a criterion handler

PredicateGenerator.CriterionHandler gains a default five-parameter handle(query, root, cb, criterion, context) that PredicateGenerator now dispatches to; its default delegates to the four-parameter method, so every existing handler, including closures coerced to the interface, keeps working unchanged. A handler that needs the context implements the new ContextualCriterionHandler (five-parameter abstract method, so a five-parameter closure coerces to it). Through JpaQueryContext.getFullyQualifiedPath(String) the handler resolves alias.property or association.property to the join GORM created for it, so a custom criterion and a GORM criterion on the same association restrict the same row rather than each joining the association again.

Ordering by an expression

A Query.Order subclass can now be converted by a registered PredicateGenerator.OrderHandler (handle(query, root, cb, order, context) returning a JPA Order, or null to leave the order out). JpaCriteriaQueryCreator.assignOrderBy looks the handler up by the order's exact class before the property path handling, so the order keeps its place among the query's orders. registerOrderHandler, orderHandler(type), clearCustomOrderHandlers and an OrderHandlerProvider ServiceLoader SPI (META-INF/services/org.grails.orm.hibernate.query.PredicateGenerator$OrderHandlerProvider) mirror the criterion ones; loadServiceProviders loads both kinds. A Query.Order subclass already reaches the creator untouched through HibernateCriteriaBuilder.order(Query.Order) → HibernateQuery.order → DetachedCriteria.order.

Tests

  • HibernateCriteriaBuilderDirectSpec: a contextual handler on children.name next to children { eq('name', 'Kid') } finds the parent whose child matches both, and finds nothing when two children match one criterion each, which proves the handler used GORM's join and not a second one; an order handler's order comes before a property order and a null return leaves it out.
  • JpaCriteriaQueryCreatorSpec: the handler's JPA order is first in orderList, the handler receives the query's context, and a null return leaves only the property order.
  • PredicateGeneratorSpec: a contextual handler receives the very context passed to getPredicates; a four-parameter handler is still called.
  • grails-test-examples/hibernate7/criteria-extension: PriceDistanceOrder with an OrderHandlerProvider discovered through META-INF/services, an orderByPriceDistance extension and ProductSearchService.findNearestPrice, with three integration cases (nearest first, position among orders, through the service); NumberLikeCriterionHandlerProvider now resolves its property through the context.

Locally (JDK 21): the three core specs 98/98, codenarcMain/codenarcTest clean on grails-data-hibernate7-core, the example app 7 unit + 18 integration specs green, and the full :grails-data-hibernate7-core:test run: 3,821 tests, 3,820 passed, 22 skipped. The one red result is SqlRestrictionHibernate7Spec > sqlRestriction works with Oracle, whose Oracle container was killed 27 s after starting on its first attempt and passed on the retry the build configures; the same image starts by hand in 15 s, and the three other Oracle specs passed in the run, so that is the local Docker environment rather than the change.

Docs

New section Extending the Criteria Builder in the GORM for Hibernate 7 guide (querying/criteria.adoc, before Method Reference), covering custom criteria and orders, the context, programmatic and ServiceLoader registration; What's New entry Hibernate 7 Criteria Queries Can Be Extended.

Base and status

This is the merge-up of #16733, the 8.1.x version of the change (@since 8.1), onto the Java-to-Groovy rewrite of the module (#16572). The two carry the same changes, this one in Groovy; review happens on #16733 first, and what lands there is merged up here, so a merge later is a hand-port into the converted files rather than a rewrite.

CI on this PR

This pull request carries the no ci label, like every open pull request except the one under review at a time, while the ASF runner pool is under pressure (#16673 gates the workflows). The temporary workflow placeholder of the first push is gone: the 2026-10-22 base merge restored the 13 trigger blocks to the base branch's. #16733 runs the full CI on 8.1.x; here, :grails-data-hibernate7-core:test and CodeNarc are green locally at every push, and the label comes off for a run before merge.

borinquenkid and others added 27 commits October 10, 2026 10:05
Converts the 24 Java sources of the spring-orm subproject (the
Hibernate 7 copies of Spring's HibernateTemplate, HibernateTransactionManager,
LocalSessionFactoryBean/Builder, SessionFactoryUtils, SpringBeanContainer,
the exception translators, OpenSessionInViewInterceptor and the
TransactionResources pair) to statically compiled Groovy under
src/main/groovy, and adds the Groovy plugin, the Groovy dependency and the
code-style plugin to its build so the new sources are compiled and
Checkstyle/CodeNarc-checked like every other framework module.

Maintainers asked in #16351 for the Java-to-Groovy migration to land one
Gradle project at a time. This is the first slice: it transplants the end
state of the 9.0.x-java-to-groovy branch for this project (original commits
2416bdd..0c2e2c2) onto 9.0.x unchanged, so that the native domain
binding work can be rebuilt on top of Groovy sources.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Converts the 207 Java main sources of grails-data-hibernate7-core
(HibernateDatastore, GrailsHibernateTemplate, the criteria builder, the
query package, the cfg/domainbinding subtree with its binders, generators
and second passes, the connections, proxy, access, support and exceptions
packages) to statically compiled Groovy, and adds two Spock specs written
while reviewing the conversion. The behaviour is unchanged: the module's
own 3657-test suite passes on the converted sources, and the review of the
original branch caught and fixed the Groovy semantics that differ from Java
(reference identity in equals(), Optional.map null-collapse, closure spread,
isX/getX accessor ambiguity).

Maintainers asked in #16351 for the Java-to-Groovy migration to land one
Gradle project at a time. This transplants the end state of the
9.0.x-java-to-groovy branch for this project (original commits
98c59ed..25dde13) onto 9.0.x unchanged. It depends on the Groovy
spring-orm subproject committed just before it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Converts the 35 Java main sources, 19 Java tests and 29 Java test fixtures
of the dbmigration-core subproject (the liquibase-hibernate fork: the
HibernateDatabase family, the snapshot and diff generators, the custom
metadata factory and the harness) to statically compiled Groovy. The
HibernateDatabase classes are annotated with @pojo as well as
@CompileStatic, because their public getProperty(String) API would
otherwise become the GroovyObject property hook and hijack dynamic
property access in the dbmigration plugin's Spock specs. Lombok is dropped
from the build, since the fixtures no longer use it, and jboss-logging is
added as compileOnly because Groovydoc resolves the documented Hibernate
types with Class.forName and hibernate-core exposes it at runtime only.

The RAT exclusions in gradle/rat-root-config.gradle that name the forked
third-party files are repointed from the old .java paths to the new
.groovy paths, so the files stay excluded and the rat task stays green.

Maintainers asked in #16351 for the Java-to-Groovy migration to land one
Gradle project at a time. This transplants the end state of the
9.0.x-java-to-groovy branch for this project (original commits
44f4279..e2ba1e6) onto 9.0.x unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…o Groovy

Converts the 5 Java sources of the Hibernate 7 Grails plugin subproject
(HibernatePersistenceContextInterceptor, GrailsOpenSessionInViewInterceptor,
AggregatePersistenceContextInterceptor and its abstract multi-data-source
base, SessionFactoryAwarePersistenceContextInterceptor) to statically
compiled Groovy. With this commit no Java source is left under
grails-data-hibernate7; the dbmigration, boot-plugin and docs subprojects
were already pure Groovy and are unchanged.

Maintainers asked in #16351 for the Java-to-Groovy migration to land one
Gradle project at a time. This transplants the end state of the
9.0.x-java-to-groovy branch for this project (original commits
ee295ea..06fb6a1) onto 9.0.x unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comparing the javap -public output of the four Hibernate 7 modules built from
origin/9.0.x (Java originals) with this branch, plus a source sweep of every
deleted .java file, found that the conversion changed what Java callers see:

- 67 constants lost their public static final field. A Groovy static final
  without public is a property (private field plus static getter), so
  Type.CONSTANT no longer compiled from Java. Restored in HibernateCriteriaBuilder
  (37), GrailsHibernateUtil (14), ClosureEventTriggeringInterceptor (10),
  IdentityEnumType (2), BackticksRemover, RootPersistentClassCommonValuesBinder,
  SessionFactoryUtils and OpenSessionInViewInterceptor.
- HibernateAlias lost its two argument constructor that defaults to
  JoinType.INNER; the Groovy record generated a same-signature constructor
  that left joinType null.
- CollectionHolder was no longer a record (no Record supertype, equals,
  hashCode or toString).
- configureNamingStrategy on GrailsDomainBinder and NamingStrategyProvider
  declared ReflectiveOperationException instead of InstantiationException and
  IllegalAccessException.
- The jspecify Nullable and NonNull annotations were dropped (about 130
  usages, mostly spring-orm: HibernateCallback.doInHibernate and the
  HibernateOperations/HibernateTemplate nullable results, parameters and
  fields). They are restored as type annotations, which Groovy emits as
  RuntimeVisibleTypeAnnotations like javac. Three binders had swapped
  jspecify NonNull for jakarta Nonnull; that is reverted.

Remaining javap differences are Groovy artifacts: extra generated record
constructors and tuple accessors, one covariant bridge method on
HibernateQuery.JpaProjectionList, an inherited bridge on
ChildHibernateDatastore, and the package-private liquibase
IdentifierGeneratorSupport now being public.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reflection spec over the public surface Java callers rely on: constants are
public static final fields, the two argument HibernateAlias constructor
joins with INNER, CollectionHolder is a record, configureNamingStrategy
declares its checked exceptions, and the nullable results of
HibernateCallback and HibernateOperations keep their Nullable annotation.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Carries #16533 (unique groups on enum and collection properties) and #16539 (nested
composite foreign key column order) over to the Groovy sources by hand:
HibernateToManyProperty.isBoundAsCollection, CreateKeyForProps, EnumTypeBinder,
GrailsPropertyBinder, PropertyFromValueCreator, BidirectionalOneToManyLinker,
CompositeIdentifierToManyToOneBinder, GrailsHibernatePersistentEntity, GrailsDomainBinder,
and CompositeForeignKeySecondPass converted to Groovy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…inator, schema and catalog

getDiscriminatorColumnName returned null for a root that maps only a discriminator value
(discriminator 'animal'), where the Java Optional chain fell back to 'class'; a one-to-many to
a table-per-hierarchy subclass then filtered on 'null' and loaded nothing. getDiscriminatorValue,
getSchema and getCatalog used ?: and also fell back on an empty string, which Optional.orElse did not.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…, keep newInstance's exceptions

- HibernateCriteriaBuilder: @slf4j did nothing on the Java class and now adds a log field that
  takes over 'log' inside criteria closures; remove it.
- HibernateClassicDatabase and JpaPersistenceDatabase: carry @pojo like the rest of the
  HibernateDatabase family so dynamic getProperty calls reach the connection property.
- NamingStrategyProvider: a strategy without a no-arg constructor is an InstantiationException again,
  and an exception from its constructor arrives unwrapped, as with Class.newInstance().

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e conversion widened

Members with no modifier in Java are public in Groovy. Put them back with @PackageScope or
protected/private: HibernateSession, HqlQueryContext helpers, GrailsHibernateTemplate,
HibernateMappingContextConfiguration, CollectionBinder, MapSecondPassBinder, PredicateGenerator,
ProjectionPredicate, SoftKey, the query delegates, IdentityEnumType.BidiEnumMap, and the
IdentifierGeneratorSupport class itself, which reads a private Hibernate field and is no API.

HqlQueryContext keeps the mutating/nativeQuery component names, with a shorter comment: under
@CompileStatic a record component named isX cannot be called as isX() from another class (Groovy 6.0.0).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… callers saw

- records: no getAt/toList/toMap/size and no default-argument constructors
- SelectHqlQuery: keep the protected field name 'delegate' and read it with this.@DeleGate
- HibernateQuery: write projections and max straight to the field; protected JpaProjectionList constructor
- try-with-resources again in HibernateDatastore, HibernateConnectionSource and HibernateConnectionSourceFactory,
  so a failing close() is wrapped or suppressed as before
- unmodifiable lists where Java returned Stream.toList()
- HibernateQueryConstants is @CompileStatic
- JavaFacingApiSpec asserts something when nothing matches; JavaCallerPin is a Java class compiled with the
  Groovy sources, so a constant that stops being a field or an exception that is no longer declared fails the build

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The five-argument constructor the specs use delegates to the six-argument one, which takes the
CreateKeyForProps; the merge had folded the two together.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…to-groovy-grails-data-hibernate7

# Conflicts:
#	gradle/rat-root-config.gradle
#	grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.groovy
#	grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPass.groovy
#	grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPass.groovy
#	grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/SetSecondPass.groovy
#	grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryContext.java
…nding (31 files)

Rebuild the 31 converted classes under cfg/domainbinding (binder, generator,
hibernate, secondpass, util) from their .java originals so git detects each
as a rename: 28 of 31 now sit at R054-R090 (were R033-R050). Only the
mechanical inverse transforms were applied (public dropped, def <T> on
generic methods, list-form @SuppressWarnings); semicolons and the original
double-quoted strings stay under scoped CodeNarc suppressions with a TODO for
the stacked groovyism-cleanup PR. Every stage-1 deviation was listed first
and preserved, proven by a normalised diff: @CompileStatic everywhere,
the nested-if rewrites of pattern-matching instanceof, closures and loops
for the Optional/stream pipelines, the IdentifierPart record as a class,
super.getIdentity()/getVersion() to avoid self-recursion, @NonNull/@nonnull
placements and the restored package-private visibility. @serial is back on
the serialVersionUID fields (no import needed).

origin/10.0.x has moved past this branch's merge-base a3f8355 for
GrailsNativeGenerator (ce72ecc), ListSecondPass and MapSecondPass
(47b836c); those three were rebuilt from the merge-base .java so no
upstream logic is merged silently here.

Still below 50%: IndexBinder R042 (the body is one static-imported Optional
pipeline that had to become if-guards), NamingStrategyWrapper R045 (three
Optional chains), GrailsNativeGenerator R046 against origin/10.0.x but R081
against the merge-base.

Verification: :grails-data-hibernate7-core compileGroovy and codenarcMain
clean; core tests 3757 run, 22 skipped, 0 failures, 0 errors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…event + plugin (27 files)

Rebuild the remaining 27 converted classes of grails-data-hibernate7-core
(query, proxy, event, multitenancy, support, access, HibernateSession,
CriteriaMethodInvoker, IdentityEnumType) and the plugin's
HibernatePersistenceContextInterceptor from their .java originals so git
detects them as renames: 26 of 27 at R053-R082 (were R034-R049). Mechanical
inverse transforms only; semicolons and double-quoted strings kept under
scoped suppressions with the groovyism-cleanup TODO; every stage-1 deviation
preserved and proven by a normalised diff (@CompileStatic, 15 @PackageScope,
28 this.@field accesses, the five .is() identity checks, def <T> methods,
closures for lambdas and streams, @nullable placements, the existing
rawtypes/PMD suppressions). A stray `};` after an if/else chain in
HqlQueryContext, which CodeNarc's SpaceAfterClosingBrace rejects, was
removed.

ByteBuddyGroovyProxyFactory, GrailsBytecodeProvider and HqlQueryContext have
drifted on origin/10.0.x since the merge-base a3f8355; they were rebuilt
from the merge-base .java so nothing upstream is merged here.

Still below 50%: HibernateQueryExecutor R043, a Java record that stage 1
turned into a Groovy record with @RecordOptions and a different body.

Verification: compileGroovy and codenarcMain clean for
:grails-data-hibernate7-core and :grails-data-hibernate7; core tests
3757 run, 22 skipped, 0 failures; plugin tests 39 run, 0 failures.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…src/main + spring-orm (31 files)

Rebuild the 30 liquibase.ext.hibernate classes of dbmigration-core's main
source set (moved from src/main/java to src/main/groovy by stage 1) and
spring-orm's AsyncRequestInterceptor from their .java originals so git
detects them as renames: 26 of 31 at R050-R097 (were R002-R049).
Mechanical inverse transforms only; semicolons and double-quoted strings
kept under scoped suppressions with the groovyism-cleanup TODO; every
stage-1 deviation preserved and proven by a normalised diff (@CompileStatic,
the @pojo Database classes, @PackageScope, this.@dialect, the loop and
if-chain rewrites of the Optional/stream pipelines, the ENVERS_* constants).
HibernateConnectionMetadata keeps `public` on its 176 DatabaseMetaData
signatures under an UnnecessaryPublicModifier suppression, which takes it
from R047 to R097.

Still below 50%: ExtendedSnapshotGenerator R023 and HibernateGenericDialect
R039 (a dozen lines each, the header dominates), HibernateChangedIndex-
ChangeGenerator R040 (stage 1 added the Apache header the original lacked),
HibernateSpringBeanDatabase R043 and JpaPersistenceDatabase R046 (stream
pipelines rewritten as loops).

Verification: compileGroovy and codenarcMain clean for
:grails-data-hibernate7-dbmigration-core and :grails-data-hibernate7-spring-orm;
dbmigration-core tests 41 run, 0 failures (testcontainers PostgreSQL).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…test and testFixtures (42 files)

Rebuild dbmigration-core's 16 test classes and 26 test fixtures (moved from
src/test/java and src/testFixtures/java to the groovy source sets by
stage 1) from their .java originals so git detects them as renames: 30 of
42 at R051-R097 (were R001-R044). Mechanical inverse transforms only,
semicolons and double-quoted strings kept; no CodeNarc suppressions are
added because the test source sets are not analysed. Every stage-1
deviation is preserved and proven by a normalised diff (@CompileStatic,
explicit JUnit static imports, the closure rewrites, the escaped `$` in
Bid.toString(), the ElementType/RetentionPolicy imports in SnowflakeId).
Authors, Posts and the two Name fixtures keep their explicit public
accessors.

Still below 50% (12 files): the Lombok fixtures, whose
`@Getter @Setter private X y;` lines must become Groovy properties because
Lombok does not run under groovyc, and the five customid/columnremarks
files whose stage-1 Apache header dominates a tiny body.

Verification: compileTestGroovy and compileTestFixturesGroovy clean;
dbmigration-core tests 41 run, 0 failures.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brings in the 260 upstream commits since the merge-base a3f8355. Because
the converted files now rename-detect against their .java originals, git
paired upstream's Java edits with the Groovy files and the conflicts were
content conflicts, resolved by hand:

- gradle/rat-root-config.gradle: the Groovy test paths stay, upstream's
  renamed Procfile entry is taken.
- CollectionBinder, SetSecondPass, ListSecondPass, MapSecondPass
  (47b836c): each collection of an embedded entity is bound with its
  own second pass.
- GrailsNativeGenerator (ce72ecc): the sequences of the default native
  generator are created on sequence-based databases; the reflection
  fallback upstream removed is gone here too.
- GrailsBytecodeProvider, GrailsProxyFactoryFactory,
  ByteBuddyGroovyProxyFactory, ByteBuddyGroovyInterceptor (0e9dc2c,
  776c6d9, 41cea2f): one shared ByteBuddyState, the interfaces kept
  on the interceptor, serialisation through SerializableGroovyProxy.
- HqlQueryContext (7008f7f, f0f8729, 85d7c86): indexOfSelect,
  indexOfFrom and indexOfTopLevel, resolveHql no longer normalises, native
  and update queries run as written.

Upstream's new proxy classes (GroovyProxyClassBuilder,
GroovyProxyMetaClassDispatcher, SerializableGroovyProxy) arrive as Java in
this commit and are converted in the next one.

Verification: compileGroovy and codenarcMain clean for core, the plugin
and dbmigration-core; core tests 3812 run, 22 skipped, 0 failures (one
transient failure, the Oracle testcontainer OOM-killed by Docker on this
machine, passed on rerun); plugin tests 39 run, 0 failures.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d upstream to Groovy

GroovyProxyClassBuilder, GroovyProxyMetaClassDispatcher and
SerializableGroovyProxy came in with the 10.0.x merge as Java. They are
converted in the same stage-2 style as the rest of the module: semicolons
and double-quoted strings kept under scoped suppressions with the
groovyism-cleanup TODO, @CompileStatic, public dropped, the .class
literals and the pattern-matching instanceof rewritten, the groovy.lang,
java.util and java.io imports dropped.

One change is not mechanical. GroovyProxyClassBuilder's delegation is now
filtered on named("intercept"): a Groovy dispatcher also declares
getMetaClass(), and ByteBuddy's name-equality resolver would bind the
proxy's getMetaClass() to that instead of intercept(..), so the wrapper
would answer the dispatcher's MetaClass. GroovyProxyClassBuilderSpec
catches it.

Verification: compile and codenarc clean; GroovyProxyClassBuilderSpec 6,
SerializableGroovyProxySpec 2, GroovyProxyMetaClassDispatcherSpec 2,
ByteBuddyGroovyProxyFactorySpec 7, ByteBuddyGroovyInterceptorSpec 13,
GrailsBytecodeProviderSpec 8, TraitInitializerProxySpec 5, all passing;
core suite 3812 run, 0 failures.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
….0.x/10.0.x syncs)

The remote branch carried two sync merges (origin/9.0.x on Oct 14, origin/10.0.x
on Oct 15) whose upstream parents are both already inside the 10.0.x merged in
b80a903. The four conflicts (List/Map/SetSecondPass, HqlQueryContext) differ
only in stage-1 vs stage-2 style over the same upstream change; the stage-2
versions are kept, so the tree is identical to d7cb780.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nless it shares a join table

Ports the 8.0.x fix for #16665 to the Groovy conversion of CollectionKeyColumnUpdater by hand:
the key is non-updatable only when another unidirectional to-many property of the owner uses the
same collection table (issue 10811), so addTo on an owner with several hasMany to different
classes is written to its join table instead of being discarded.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Hibernate 7 resolves the type name of a BasicValue through
TypeDefinition, which accepts a BasicType or a UserType only, so a
property mapped with `type: MonetaryAmountType` where that class
implements CompositeUserType failed the SessionFactory build with
"Named type [...] did not implement BasicType nor UserType". Hibernate
binds a @CompositeType as a Component whose type name is the user type,
with one property per attribute of the embeddable() class in
alphabetical order, read through getPropertyValue(value, index) and
rebuilt through instantiate(ValueAccess).

GrailsPropertyBinder now routes a property whose type, or the user-type
registered for its class, implements CompositeUserType to the new
CompositeUserTypeBinder, which builds that Component: the columns of
the property mapping are assigned to the attributes in alphabetical
order and bound through ColumnBinder, an attribute without a column is
named after the property and the attribute, and more columns than
attributes is a MappingException. typeParams are logged as ignored,
since Hibernate instantiates a composite user type itself.

Documents composite user types in the GORM guide, the `type` mapping
reference and the Grails 8 upgrade guide.

Fixes #16688
… handler order by an expression

A custom criterion handler could not resolve a property path the way the built-in criteria do: it
got the query, its root and the criteria builder, but not the JpaQueryContext that holds the joins
and aliases GORM created, so a handler had to join an association itself, and a GORM criterion on
the same alias then joined it again and the two could match different rows. CriterionHandler gains
a default five-parameter handle(..., JpaQueryContext context) that PredicateGenerator dispatches
to; its default calls the four-parameter method, so existing handlers and closures keep working.
ContextualCriterionHandler is the five-parameter functional form, through which
JpaQueryContext.getFullyQualifiedPath resolves alias.property or association.property to the join
GORM made for it.

Ordering by an expression had no hook at all: assignOrderBy ordered by property paths only, so a
plugin had to insert into the query's order list from a criterion and hope the orders were
assigned first. A Query.Order subclass is now converted by a registered OrderHandler, looked up by
its exact class before the property path handling, so it keeps its place among the orders and can
return null to leave the order out. registerOrderHandler, orderHandler, clearCustomOrderHandlers
and an OrderHandlerProvider ServiceLoader SPI mirror the criterion ones.

The criteria-extension example app gains PriceDistanceOrder with a provider discovered through
META-INF/services, an orderByPriceDistance extension and a service method, and its NumberLike
handler now resolves its property through the context. The Hibernate 7 guide documents extending
the criteria builder, and the What's New notes it.

Fixes #16563
… PR (revert before merge)

Pushes, rebases and force-pushes to this stacked PR start no jobs; the PR closest to the release branch
keeps CI. Merging with this commit is a blocker.
Copilot AI balanced review requested due to automatic review settings October 20, 2026 18:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…hether a key is updatable

Port of 7138500 (#16686) to the Groovy conversion. CollectionKeyColumnUpdater asked every
unidirectional to-many of the owner for its Hibernate collection; a to-many mapped with
type: 'serializable' or with a user type is bound as a column and has none, and getCollection()
throws a MappingException rather than returning null, so such an owner failed the session factory
build. Filter the siblings with HibernateToManyProperty.isBoundAsCollection.

CollectionKeyColumnUpdaterSpec and MultipleUnidirectionalHasManySpec carry the cases of #16686.

@jamesfredley jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not approving. Reviewed only the delta on top of the Java-to-Groovy Hibernate 7 branch, not the conversion. The four-argument handler contract and ordinary order translation look right, and there is no request-scoped context stored on a static.

Two problems block this. Provider discovery publishes the initialized flag before the order handlers are loaded, so a second request during first use can miss a new order handler and sort by the property name instead. And this delta sets 13 workflow pull_request filters to branches: ['CHANGEME-FOR-ACCEPTANCE'], so this pull request and ordinary release-targeted pull requests no longer run those checks.

The new translator tests call PredicateGenerator.getPredicates() and JpaCriteriaQueryCreator.createQuery() directly. That is not blocking on its own. Please also cover the order handler through withCriteria or DetachedCriteria.list().

Comment thread .github/workflows/gradle.yml Outdated
- '[0-9]+.[0-9]+.x'
pull_request:
# CHANGEME FOR ACCEPTANCE: PR triggers disabled on this stacked branch; revert before merge
branches: ['CHANGEME-FOR-ACCEPTANCE']

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

branches here filters the pull request base. CHANGEME-FOR-ACCEPTANCE matches no release branch, so this workflow does not run for this pull request or for a pull request targeting 9.0.x / 8.0.x / 7.0.x.

The same placeholder is in the other 12 workflow files in this delta: benchmark, build-health, codeanalysis, codeql, codestyle, coverage, end-to-end, groovy-snapshot-canary, rat, sitemesh2, validate-actions, and vulnerability-scan.

Restore those trigger blocks to the base versions before merge.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Restored: the base merge of 2026-10-22 brought the policy workflows (#16673) and the 13 trigger blocks are identical to the base branch again; the placeholder is gone from this branch.

…oovy-grails-data-hibernate7

# Conflicts:
#	grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.groovy
#	grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.groovy
#	grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/ManyToOneValuesBinder.groovy
#	grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ToManyEntityMultiTenantFilterBinder.groovy
#	grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/multitenancy/MultiTenantEventListener.groovy
…at/criteria-handler-context

# Conflicts:
#	.github/workflows/benchmark.yml
#	.github/workflows/build-health.yml
#	.github/workflows/codeanalysis.yml
#	.github/workflows/codeql.yml
#	.github/workflows/codestyle.yml
#	.github/workflows/coverage.yml
#	.github/workflows/end-to-end.yml
#	.github/workflows/gradle.yml
#	.github/workflows/groovy-snapshot-canary.yml
#	.github/workflows/rat.yml
#	.github/workflows/sitemesh2.yml
#	.github/workflows/validate-actions.yml
#	.github/workflows/vulnerability-scan.yml
- Handler discovery waits for the providers. `loadServiceProviders` loads
  under a lock and publishes its flag only after both ServiceLoader loops,
  so a query that arrives during the discovery waits for it instead of
  deciding, with the handlers still unregistered, that an order has no
  handler. A handler registered in code stays ahead of a discovered one.
  OrderHandlerDiscoverySpec blocks a thread inside a provider found through
  META-INF/services and shows a second lookup waiting for the handler; it
  fails with the previous flag.
- Handlers see a `QueryPathContext`, not the translation state. The new
  interface has `getRoot()`, `getFullyQualifiedPath(String)` and
  `getFullyQualifiedExpression(String)`; `JpaQueryContext` implements it, and
  `CriterionHandler`, `ContextualCriterionHandler`, `OrderHandler` and the
  guide depend on the interface only.
- A count query leaves its orders out, handled ones too (the fix of the
  8.0.x count query, carried here until it merges up), so the guide no
  longer asks an order handler to return null for a count, and a `count()`
  with a handled order counts every row.
- Tests: the custom criterion before the association block resolves to the
  same join; the order handler through `DetachedCriteria.list()`.
@borinquenkid

Copy link
Copy Markdown
Member Author

Follow-up pushed as b39597e: the order handler is also covered through DetachedCriteria.list(), and with the count fix (#16731) in this branch too, a count() with a handled order counts every row and the docs no longer ask handlers to return null for a count. The pull request stays no ci; :grails-data-hibernate7-core:test and CodeNarc are green locally.

@borinquenkid

Copy link
Copy Markdown
Member Author

Yes: this goes to 8.x first. I am porting it to 8.1.x as a minor feature (@since 8.1), so grails-postgresql-extensions can drop its workaround on Grails 8; this pull request then becomes the merge-up of that one onto the Java-to-Groovy branch.

@borinquenkid

Copy link
Copy Markdown
Member Author

The 8.1.x version is #16733. It carries the same changes as the latest push here, in Java, with @since 8.1; this pull request becomes its merge-up onto the Java-to-Groovy branch.

@jamesfredley jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The first-use discovery race is fixed. Both provider types load under one lock, and the initialized flag is published after that. Not approving. This pull request still carries no ci, so the build jobs are skipped, and a custom order cannot see a sqlProjection alias.

…cords under its alias

A custom order that names a sqlProjection alias could not reach that expression: the translator records it with registerSelectionAlias, and getFullyQualifiedPath and getFullyQualifiedExpression resolve property paths only. QueryPathContext now exposes getSelectionAlias(String), which JpaQueryContext already had, so an order handler orders by a projected value the way the built-in alias order does; predicate property resolution is unchanged. HibernateCriteriaBuilderSqlProjectionSpec orders by a sqlProjection alias through a handler and asserts the rows come in the projection's order and the SQL orders by the select item, not by a column. The guide names the accessor.
…commit): same tree, so this records the new history only

@jamesfredley jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving the alias lookup. A custom order can call getSelectionAlias and get the expression a sqlProjection recorded. The discovery-race thread stays resolved. This pull request still has the no ci label, so GitHub will not run the build until that label is removed.

…at/criteria-handler-context

Claude-Session: https://claude.ai/code/session_01MDHn7fC3hsGJ27vFHD2o1p

# Conflicts:
#	grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaCriteriaQueryCreator.groovy
#	grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy
#	grails-doc/src/en/guide/introduction/whatsNew.adoc
…sources

Ports what #16733 changed after the first port, from 1e47f7e to 3bdd024, into the converted
classes: a closure registers as a criterion handler without as (five parameters make it contextual), the order
handler map is private, discovery and registration are documented as putIfAbsent and put,
getFullyQualifiedExpression resolves like the built-in predicates and points to getSelectionAlias for the column
alias of a sqlProjection, QueryPathContext has the ASF header, and the specs and the guide follow. The count-only
check is the base's; the duplicate this branch carried is gone.

Claude-Session: https://claude.ai/code/session_01MDHn7fC3hsGJ27vFHD2o1p
@borinquenkid
borinquenkid merged commit 94ba143 into refactor/java-to-groovy-grails-data-hibernate7 Oct 25, 2026
45 checks passed
codeconsole pushed a commit to codeconsole/grails-core that referenced this pull request Oct 26, 2026
…ler order by an expression

Resolves apache#16563 on 8.x. A plugin or an application can extend the
Hibernate 7 criteria builder with its own criteria and orders.

- A criterion handler can take the query's QueryPathContext, a small
  interface that JpaQueryContext implements (getRoot, getFullyQualifiedPath,
  getFullyQualifiedExpression). Through it a handler resolves
  alias.property or association.property to the join GORM created for the
  association, so a custom criterion and a built-in one on the same
  association restrict the same row instead of each joining it again. The
  four-argument handler keeps working; ContextualCriterionHandler is the
  five-argument form, and a closure with five parameters is coerced to it.
- An OrderHandler turns a Query.Order subclass into a JPA order by any
  expression, in its place among the query's orders, registered in code or
  through a META-INF/services OrderHandlerProvider.
- Handler discovery waits for the providers: loadServiceProviders loads
  under a lock and publishes its flag only after both ServiceLoader loops,
  so a query that arrives during the discovery no longer decides, with the
  handlers still unregistered, that an order has no handler. A handler
  registered in code stays ahead of a discovered one.
- A count query leaves its orders out, handled ones too (the fix of the
  previous commit), so no handler runs for a count.

Covered by HibernateCriteriaBuilderDirectSpec (a contextual handler on an
association path before and after the association block, an order handler
through the builder and DetachedCriteria.list(), a count with a handled
order), OrderHandlerDiscoverySpec (a lookup during a blocked discovery
waits for the handler), PredicateGeneratorSpec and
JpaCriteriaQueryCreatorSpec. Documented in the Hibernate 7 guide and the
what's new; the criteria-extension example exercises both handler kinds
through providers.

This is the 8.1.x version of apache#16696, which becomes its merge-up onto the
Java-to-Groovy Hibernate 7 branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants