Skip to content

Hibernate 7#15568

Open
jdaugherty wants to merge 1166 commits into8.0.xfrom
8.0.x-hibernate7
Open

Hibernate 7#15568
jdaugherty wants to merge 1166 commits into8.0.xfrom
8.0.x-hibernate7

Conversation

@jdaugherty
Copy link
Copy Markdown
Contributor

Reopening. This PR replaces #15530

jamesfredley and others added 30 commits April 2, 2026 10:19
Move multi-line @requires closure statements to new line after opening
brace in FindByMethodSpec. Remove unused GrailsTransactionTemplate
import from OptimisticLockingSpec.

Assisted-by: OpenCode <opencode@opencode.ai>
…rage

Add 15 new test methods (from 5 to 20 total) covering all ProxyHandler
and ProxyFactory contract methods: isInitialized (null, non-proxied,
persistent collection, Hibernate proxy, Groovy proxy), unwrap (Hibernate
proxy, Groovy proxy, non-proxy), getIdentifier (Hibernate proxy,
non-proxied, null), createProxy, and isProxy (non-Hibernate object).

The Groovy proxy isInitialized test is excluded because it depends on a
source fix to HibernateProxyHandler.isInitialized() that will be
submitted in a separate PR.

Assisted-by: OpenCode <opencode@opencode.ai>
The Hibernate 7 HibernateProxyHandler.isInitialized() was missing a check
for Groovy proxies (ProxyInstanceMetaClass), causing uninitialized Groovy
proxies to be incorrectly reported as initialized. This is because the
fallback Hibernate.isInitialized() returns true for any non-Hibernate
object.

Add GroovyProxyInterceptorLogic.isInitialized() helper that checks
ProxyInstanceMetaClass.isProxyInitiated(), and call it from
HibernateProxyHandler.isInitialized() before the Hibernate fallback.

The Hibernate 5 implementation already had this check. This restores
parity and adds the corresponding test.

Assisted-by: OpenCode <opencode@opencode.ai>
- Rename groovyProxyInit to groovyProxyInitialized for clearer state semantics
- Add ProxyInstanceMetaClass precondition assertion in Groovy proxy test

Assisted-by: OpenCode <opencode@opencode.ai>
Hibernate 7's SqmCriteriaNodeBuilder.valueParameter() throws
HibernateException when encountering Groovy's GStringImpl, unlike
Hibernate 5 which silently coerced it. Add normalizeValue() to
PredicateGenerator that converts CharSequence (including GString) to
String before passing values to JPA Criteria predicates.

Applied to Equals, NotEquals, IdEquals, SizeEquals, SizeNotEquals, and
Between handlers. Like/ILike already called toString() and were
unaffected. Removes @PendingFeature from MultipleDataSourceConnectionsSpec
which now passes all 4 tests.

Assisted-by: OpenCode <opencode@opencode.ai>
…query

fix: normalize Groovy GString to String in Hibernate 7 criteria queries
…in H7 binding

Introduce HibernateSimpleIdentityProperty and HibernateCompositeIdentityProperty
as proper GORM persistent property types, replacing direct use of the DSL config
objects (HibernateSimpleIdentity / HibernateCompositeIdentity) in the binding
pipeline.

- Add HibernateSimpleIdentityProperty and HibernateCompositeIdentityProperty
  (both extend HibernateIdentityProperty)
- Add HibernatePersistentEntity.getIdentityProperty() returning the typed
  identity property (simple or composite)
- HibernateMappingFactory.createIdentity() now returns HibernateSimpleIdentityProperty
- Merge two BasicValueCreator.bindBasicValue() overloads into a single
  bindBasicValue(HibernatePersistentProperty) — the property supplies its own
  table and owner, removing redundant parameters
- Remove Table parameter from SimpleIdBinder.bindSimpleId() and its callers
  (IdentityBinder, SimpleValueBinder)
- Update all H7 unit specs (IdentityBinderSpec, SimpleIdBinderSpec,
  SimpleValueBinderSpec, BasicValueCreatorSpec) to match the new API

Fix pre-existing TCK failures on Hibernate 7 (never passing in isolation):
- EnumSpec: removed duplicate V1 EnumThing records in 3 feature methods that
  caused findByEn() to throw NonUniqueResultException
- FindByMethodSpec.testBooleanPropertyQuery: reduced Highway fixture to 1
  bypassed + 1 not-bypassed record; adjusted findAll* count assertions;
  removed TckBook.findPublished() unique-finder assertion against 3 records

These TCK tests were broken since commit 20131c0 ('fix: restore non-hibernate7
test coverage') due to Hibernate 7 strict getSingleResult() enforcement. The
full top-level build did not surface them because the grails-datamapping-tck
jar was built before those feature methods existed in the compiled artifact.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erty

Add getGeneratorName() as a default method on HibernatePersistentProperty
that reads from the property's mapped form. HibernateSimpleIdentityProperty
overrides it to delegate to the owning entity's getIdentityGeneratorName(),
replacing the instanceof check that lived in BasicValueCreator.

- HibernatePersistentProperty.getGeneratorName(): default reads PropertyConfig.getGenerator()
- HibernateSimpleIdentityProperty.getGeneratorName(): delegates to entity identity generator name
- BasicValueCreator.bindBasicValue(): simplified to property.getGeneratorName() with
  Optional.ofNullable().ifPresent() fluent style; no longer needs instanceof check
- BasicValueCreator.createGenerator(): drop unused HibernateSimpleIdentityProperty parameter
- BasicValueCreatorSpec: stub identityProperty.getGeneratorName() directly;
  remove dead domainClass.getIdentityGeneratorName() and getRootClass() stubs;
  remove unused RootClass entity field

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Verify the new getGeneratorName() default method and its override:

- Regular property with no generator configured → returns null (default impl reads PropertyConfig.getGenerator())
- HibernateSimpleIdentityProperty with no explicit generator → delegates correctly to entity.getIdentityGeneratorName() and is non-null
- HibernateSimpleIdentityProperty with id generator: 'uuid2' → returns 'uuid2'

Adds GeneratorDefaultEntity and GeneratorUuid2Entity test fixtures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…returning null

HibernatePersistentEntity wraps a Hibernate RootClass which always has an
identifier. Returning null was misleading — callers would silently get NPEs
downstream. Now getIdentityProperty() throws MappingException with a clear
message if it is called on an entity that has no HibernateSimpleIdentityProperty
(a programming error; only embedded entities have no identity, and they are not
HibernatePersistentEntity instances).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- SimpleIdBinder.bindSimpleId: remove RootClass and HibernateSimpleIdentityProperty
  params; derive from domainClass internally; fix always-throw bug (add return)
- CompositeIdBinder.bindCompositeId: remove RootClass and HibernateCompositeIdentity
  params; derive from domainClass internally; fix always-throw bug (add return)
- IdentityBinder: call single-arg binders; set persistentClass from root before dispatch
- HibernateCompositeIdentityProperty: change parts type from
  HibernateSimpleIdentityProperty[] to HibernatePersistentProperty[] so regular
  composite key fields (HibernateSimpleProperty) are not filtered out
- HibernatePersistentEntity.getIdentityProperty: pass all composite parts directly
  without filtering by type
- Update all specs: CompositeIdBinderSpec, SimpleIdBinderSpec, IdentityBinderSpec,
  HibernatePersistentPropertySpec (add composite key regression test)
- AGENTS.md: add rule 11 — every code touch must update all tests for the changed class

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
setPersistentClass is already called in ClassBinder.bindClass before
bindIdentity is invoked, making the param and the call redundant.
Update RootPersistentClassCommonValuesBinder and IdentityBinderSpec accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract performUpsert() from save() in HibernateGormInstanceApi
  Routes to performPersist (id==null) or performMerge (id set)
- Override GormStaticApi.merge(D d) in HibernateGormStaticApi
  to delegate to instanceApi.merge(d) instead of session.persist(d)
  which throws on detached entities in Hibernate 7
- Fix performMerge to sync id (before flush) and version (after flush)
  back to the caller's instance, matching Hibernate 5 mutation semantics
- Add two tests: merge on new instance gets id+version=0;
  merge on detached instance keeps id, version increments to 1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Existing test: list(max:N) and list(offset:N, max:M) -> PagedResultList
- New test: list() and list(offset:N) without max -> plain List, not PagedResultList

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ria.list

withPopulatedQuery already calls populateArgumentsForCriteria unconditionally.
The extra call inside the args?.max branch caused duplicate ORDER BY clauses
since Query.order() is additive (orderBy.add). Remove the redundant call.

Add test to verify sort+max returns results in correct order exactly once.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ate7

# Conflicts:
#	NOTICE
#	gradle/publish-root-config.gradle
- Add Query.clearOrders() to core — clears orderBy list; subclasses override
- Override HibernateQuery.clearOrders() to also clear JPA detachedCriteria orders,
  replacing the TODO HACK that used order(null) to piggyback order clearing
- HibernateQuery.order() now unconditionally delegates to detachedCriteria (no null check)
- PagedResultList.initialize() calls clearOrders() on cloned query before COUNT,
  replacing the direct orderBy.clear() which missed HibernateQuery's JPA orders
- DetachedCriteria.list() uses protected newPagedResultList() factory method
- Tests: HibernateQuerySpec (clearOrders), PagedResultListSpec (DetachedCriteria
  sort+max totalCount), DetachedCriteriaSpec TCK (sort+max+totalCount)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a second bypassed and not-bypassed Highway row so findAllBypassed()
and findAllNotBypassed() actually validate multi-row behavior.
Replace findNotBypassed()/findBypassed() singular calls (which now
throw NonUniqueResultException with 2 rows) with findBypassedByName/
findNotBypassedByName to keep singular finders unambiguous.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When actionName is null and no id is present, append the controller token
so that <g:form controller="foo"> (no action) generates /foo instead of
an empty string. Preserve existing behaviour when an id is present and
no action is given (id-only URL patterns such as /$id? still resolve
correctly without prepending the context controller name).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 'many-to-many queries with sorting do not throw exception' feature
(issue #14636) now passes on Hibernate 5 as well. Remove the
@PendingFeatureIf guard and the unused IgnoreIf/PendingFeatureIf imports.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jdaugherty
Copy link
Copy Markdown
Contributor Author

FYI: I think I found out why testcontainers kept getting added to the boms - grails-forge is a micronaut app that still needs the older version. I've updated the micronaut dependencies to pull from the forge bom instead.

@borinquenkid
Copy link
Copy Markdown
Member

borinquenkid commented May 3, 2026 via email

@jdaugherty
Copy link
Copy Markdown
Contributor Author

@borinquenkid FYI: the forge tests are passing with the changes. All of the grails projects are using the newer version from the spring bom.

@borinquenkid
Copy link
Copy Markdown
Member

Cool.

jdaugherty added 2 commits May 3, 2026 19:40
# Conflicts:
#	grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/build/dependencies/GradleDependencyComparatorSpec.groovy
@jdaugherty
Copy link
Copy Markdown
Contributor Author

I split the micronaut bom into 2 - 1 for hibernate 5 & 1 for hibernate 7. Also, as part of the merge I lowered the memory back down and the tests are passing.

At this point, I need to review the licensing for the liquibase-hibernate import & then we can review this PR.

@jdaugherty
Copy link
Copy Markdown
Contributor Author

@borinquenkid I need your help - do you have a base branch of the liquibase library that you imported? I need to clearly know which files were brought into the project (we can't put our own headers on them).

I'm assuming it's the files under: grails-data-hibernate7/dbmigration/src/main/java ?

@jdaugherty
Copy link
Copy Markdown
Contributor Author

Per discussion with @borinquenkid 4.27 liquibase-hibernate is what was imported.

This means: https://github.com/liquibase/liquibase-hibernate/tree/v4.27.0-hibernate5 is the branch

jdaugherty added 4 commits May 4, 2026 12:27
Import the original source from https://github.com/liquibase/liquibase-hibernate
branch v4.27.0-hibernate5 into grails-data-hibernate7/dbmigration-core without
modification. This establishes the upstream baseline for subsequent Hibernate 7
adaptation changes.

The build.gradle and gradle.properties are new files to integrate this as a
Gradle subproject within the grails-core build.
…ject

Separate the liquibase-hibernate extension code from the dbmigration plugin
into its own subproject (grails-data-hibernate7-dbmigration-core). This makes
the dependency boundary clear: dbmigration-core contains the upstream forked
library, while dbmigration contains the Grails-specific plugin code.

- Remove liquibase/ext source and tests from dbmigration
- Update dbmigration to depend on dbmigration-core
- Update service files to keep only Grails-specific entries in dbmigration
- Rename snapshot/diff generators with 'Hibernate' prefix for clarity
- Extract NoOpConnectionProvider and NoOpMultiTenantConnectionProvider
  from inner classes to standalone classes
- Remove deprecated Hibernate 5 APIs: ClassLoaderDelegate,
  MetadataBuilderImpl, USE_NEW_ID_GENERATOR_MAPPINGS
- Replace Class.newInstance() with Constructor.newInstance() reflection
- Update Hibernate 7 API calls (MetadataBuilder, BootstrapServiceRegistry)
- Update META-INF service files to match renamed classes
- Update test fixtures for Hibernate 7 compatibility
@jdaugherty
Copy link
Copy Markdown
Contributor Author

I've reimported the code under dbmigration-core (dropping the liquibase name since it's trademarked). I've followed the guidelines here: https://www.apache.org/legal/src-headers.html#3party Which explicitly say do not add source headers. I've made 3 commits to illustrate the original forked code vs what Walter updated.

I believe the licensing side of this is "right" now, but @jamesfredley do you think we should have someone in the ASF review this part of the contribution?

@testlens-app
Copy link
Copy Markdown

testlens-app Bot commented May 5, 2026

✅ All tests passed ✅

🏷️ Commit: 23649db
▶️ Tests: 36559 executed
⚪️ Checks: 34/34 completed


Learn more about TestLens at testlens.app.

Copy link
Copy Markdown
Contributor Author

@jdaugherty jdaugherty left a comment

Choose a reason for hiding this comment

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

Walter this is an incredible amount of work.

@jdaugherty
Copy link
Copy Markdown
Contributor Author

I did some more minor cleanup & removed spotless since it's not being used right now. #15555 will add automated formatting

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants