Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e649743
refactor: change SimpleValueBinder constructor visibility to private
borinquenkid Apr 1, 2026
6929305
refactor: BasicValueIdCreator and SimpleValueBinder refactoring
borinquenkid Apr 1, 2026
1a7eb80
refactor: encapsulate identifier resolution in BasicValueIdCreator
borinquenkid Apr 1, 2026
2cf2ad1
refactor: rename BasicValueIdCreator to BasicValueCreator and consoli…
borinquenkid Apr 1, 2026
ecf8caf
refactor: rename of HibernateProperty hierarchy
borinquenkid Apr 2, 2026
6fd8c13
Merge remote-tracking branch 'origin/8.0.x-hibernate7' into 8.0.x-hib…
borinquenkid Apr 2, 2026
1a66edc
Merge branch '8.0.x-hibernate7' into 8.0.x-hibernate7-dev
borinquenkid Apr 2, 2026
2687842
refactor: replace DSL identity config with typed identity properties …
borinquenkid Apr 2, 2026
101d0bc
Merge branch '8.0.x-hibernate7' into 8.0.x-hibernate7-dev
borinquenkid Apr 2, 2026
fbced2d
refactor: move generator name resolution into HibernatePersistentProp…
borinquenkid Apr 2, 2026
14dd3f8
test: add getGeneratorName() coverage to HibernatePersistentPropertySpec
borinquenkid Apr 2, 2026
67fb91e
refactor: throw MappingException in getIdentityProperty() instead of …
borinquenkid Apr 2, 2026
a7830a6
refactor: simplify binder signatures; fix composite identity parts type
borinquenkid Apr 3, 2026
e0b59e1
refactor: remove RootClass param from IdentityBinder.bindIdentity
borinquenkid Apr 3, 2026
8bdc7e3
fix: H7 merge upserts correctly and mutates caller instance
borinquenkid Apr 3, 2026
93c5928
test: DetachedCriteria list returns PagedResultList only when max is set
borinquenkid Apr 3, 2026
6382464
fix: remove double populateArgumentsForCriteria call in DetachedCrite…
borinquenkid Apr 3, 2026
945c907
fix: PagedResultList totalCount leaks ORDER BY into COUNT query on H2
borinquenkid Apr 3, 2026
8c2ae2e
test: restore multi-row validation for findAll* boolean property finders
borinquenkid Apr 3, 2026
eaacee2
H7: incorrect messages
borinquenkid Apr 3, 2026
78b8b36
Merge branch '8.0.x-hibernate7' into 8.0.x-hibernate7-dev
borinquenkid Apr 3, 2026
1497ea4
H7: fix style violations and update agent guidelines
borinquenkid Apr 3, 2026
9d723a8
H7: update agent guidelines for test aggregation
borinquenkid Apr 3, 2026
d52a9e5
H7: make test aggregation more robust by scanning all root directories
borinquenkid Apr 3, 2026
6658a08
fix: DefaultUrlCreator generates correct URL for controller-only links
borinquenkid Apr 3, 2026
9c79935
fix: remove @PendingFeatureIf from many-to-many sorting test in H5
borinquenkid Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export GRADLE_OPTS="-Xms2G -Xmx5G"
8. **No internal APIs in docs** - Only document public APIs; never reference internal or package-private classes and methods in user-facing documentation
9. **Test via public APIs** - Tests must exercise behavior through the same APIs an end user calls; never invoke internal implementations, package-private methods, or bypass the public surface directly
10. **Always review and extend tests** - Review existing unit and functional tests before making changes; every code change must include new or enhanced tests that cover the affected behavior
11. **Every code touch must update all tests for the changed class** - When a class is modified, find and update every test that covers it — unit, integration, and TCK. Do not leave any existing test out of sync with the new code.
12. **Clean violations before commit** - Before every automated commit, run `./gradlew clean test aggregateTestFailures --continue` from the root and ensure that `TEST_FAILURES.md` reports no issues and is removed.

## Available Skills

Expand Down Expand Up @@ -229,8 +231,9 @@ class MyService { }
1. **Fork & branch** from the target release branch (e.g., `7.0.x`)
2. **Run tests** before submitting: `./gradlew build --rerun-tasks`
3. **Run code style checks**: `./gradlew codeStyle`
4. **Squash commits** into a single meaningful commit message
5. **Reference issues** in PR description (e.g., "Fixes #1234")
4. **Clean style violations**: Before committing, run `./gradlew clean aggregateStyleViolations` from the root and ensure that `CHECKSTYLE_VIOLATIONS.md`, `CODENARC_VIOLATIONS.md`, and `PMD_VIOLATIONS.md` have no issues.
5. **Squash commits** into a single meaningful commit message
6. **Reference issues** in PR description (e.g., "Fixes #1234")

### Review Process

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ class GrailsTestPlugin implements Plugin<Project> {
slurper.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
slurper.setFeature("http://xml.org/sax/features/namespaces", false)

project.subprojects.each { subproject ->
def testResultsDir = subproject.layout.buildDirectory.dir("test-results").get().asFile
if (testResultsDir.exists()) {
def processedDirs = [] as Set

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.

It's not clear to me why we need this plugin when the aggregate test project already exists. @borinquenkid I'm going to go ahead and merge this PR and we can re-address on the hibernate PR.

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.

This one aggregates test failures across all modules into a single .gitignored markdown file. If one runs it as a developer from root it can pickup multiple regressions and tackle them without having to rerun again.
There is another feature that does that for the three linting checks. None of them are part of the build process.

def processTestResults = { File testResultsDir, String moduleName ->
if (testResultsDir.exists() && processedDirs.add(testResultsDir.absolutePath)) {
testResultsDir.eachFileRecurse { file ->
if (file.name.endsWith('.xml') && file.name.startsWith('TEST-')) {
try {
Expand All @@ -61,7 +62,7 @@ class GrailsTestPlugin implements Plugin<Project> {
if (testcase.failure.size() > 0 || testcase.error.size() > 0) {
def failure = testcase.failure.size() > 0 ? testcase.failure[0] : testcase.error[0]
failures << [
module: subproject.name,
module: moduleName,
className: testcase.@classname.text(),
testName: testcase.@name.text(),
message: failure.@message.text() ?: failure.text().take(200),
Expand All @@ -77,6 +78,24 @@ class GrailsTestPlugin implements Plugin<Project> {
}
}

// 1. All subprojects
project.subprojects.each { subproject ->
def testResultsDir = subproject.layout.buildDirectory.dir("test-results").get().asFile
processTestResults(testResultsDir, subproject.name)
}

// 2. Any other directory in root that might have test results (like grails-docs or build-logic)
project.layout.projectDirectory.asFile.eachDir { dir ->
if (!dir.name.startsWith('.') && dir.name != 'build' && dir.name != 'node_modules') {
def testResultsDir = new File(dir, "build/test-results")
processTestResults(testResultsDir, dir.name)

// Also check for results in the directory itself (if it's a build output)
def directTestResultsDir = new File(dir, "test-results")
processTestResults(directTestResultsDir, dir.name)
}
}

writeReport(project, failures)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@
*/
package grails.gorm.tests

import spock.lang.IgnoreIf
import spock.lang.PendingFeatureIf

import grails.gorm.annotation.Entity
import grails.gorm.hibernate.HibernateEntity
import grails.gorm.transactions.Rollback
Expand Down Expand Up @@ -160,7 +157,6 @@ class WhereQueryOldIssueVerificationSpec extends Specification {

@Rollback
@Issue('https://github.com/apache/grails-core/issues/14636')
@PendingFeatureIf({ System.getProperty('hibernate5.gorm.suite') })
def "many-to-many queries with sorting do not throw exception"() {
given: "users and roles in a many-to-many relationship"
def role1 = new WqRole(name: "ADMIN").save(flush: true)
Expand Down
13 changes: 8 additions & 5 deletions grails-data-hibernate7/HIBERNATE7-BINDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ The `bindProperty` method in `GrailsPropertyBinder.java` has been successfully r
* **IdentityBinder**: Main coordinator for binding entity identifiers (simple or composite).
* **SimpleIdBinder**: Binds simple primary keys.
* **CompositeIdBinder**: Binds composite primary keys.
* **BasicValueIdCreator**: Factory for creating identifier `Value` objects and their generators.
* **BasicValueCreator**: Factory for creating identifier `Value` objects and their generators.
* **VersionBinder**: Binds the version property used for optimistic locking.
* **NaturalIdentifierBinder**: Binds properties marked as `naturalId`.

Expand Down Expand Up @@ -163,7 +163,7 @@ The `bindProperty` method in `GrailsPropertyBinder.java` has been successfully r

| Class | Status | Notes |
| :--- | :--- | :--- |
| `CollectionSecondPassBinder` | Migrated | Contains TODOs for unidirectional many-to-many. |
| `CollectionSecondPassBinder` | Migrated | Unidirectional many-to-many support implemented. |
| `GrailsSecondPass` | Migrated | |
| `ListSecondPass` | Migrated | |
| `ListSecondPassBinder` | Migrated | |
Expand Down Expand Up @@ -197,7 +197,7 @@ The `bindProperty` method in `GrailsPropertyBinder.java` has been successfully r
| `TableForManyCalculator` | Migrated | |
| `UniqueNameGenerator` | Migrated | |
| `BackticksRemover` | Migrated | |
| `BasicValueIdCreator` | Migrated | |
| `BasicValueCreator` | Migrated | |

## Utility Class Refactoring & Mock Compatibility

Expand All @@ -216,6 +216,9 @@ The `bindProperty` method in `GrailsPropertyBinder.java` has been successfully r

## Remaining Known Issues / TODOs

- `CollectionSecondPassBinder`: TODO support unidirectional many-to-many.
- `GrailsIncrementGenerator`: Reflection hacks for Hibernate 7 (scheduled for removal in Hibernate 8).
- **Multitenancy & CompositeId:** While many tests are passing, some complex scenarios in `MultiTenancyBidirectionalManyToManySpec` and `GlobalConstraintWithCompositeIdSpec` may still require attention or further validation in a full application context.

## Resolved Issues

- `CollectionSecondPassBinder`: Unidirectional many-to-many support implemented.
- **Multitenancy & CompositeId:** `MultiTenancyBidirectionalManyToManySpec` and `GlobalConstraintWithCompositeIdSpec` have been fixed and validated.
40 changes: 0 additions & 40 deletions grails-data-hibernate7/HIBERNATE_7_MIGRATION.md

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,7 @@ class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
validateable.skipValidation(true)

try {
String idPropertyName = domainClass.identity?.name ?: 'id'
Object idVal = InvokerHelper.getProperty(target, idPropertyName)
if (idVal == null) {
return performPersist(target, shouldFlush)
} else {
return performMerge(target, shouldFlush)
}
return performUpsert(target, shouldFlush)
} finally {
validateable.skipValidation(false)
}
Expand Down Expand Up @@ -240,14 +234,33 @@ class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
return instance
}

protected D performUpsert(D target, boolean shouldFlush) {
PersistentEntity entity = persistentEntity
String idPropertyName = entity.identity?.name ?: 'id'
Object idVal = InvokerHelper.getProperty(target, idPropertyName)
if (idVal == null) {
return performPersist(target, shouldFlush)
} else {
return performMerge(target, shouldFlush)
}
}

protected D performMerge(final D target, final boolean flush) {
hibernateTemplate.execute { Session session ->
Object merged = session.merge(target)
D merged = (D) session.merge(target)
session.lock(merged, LockModeType.NONE)
// Sync id back immediately so target has an identity
String idProp = persistentEntity.identity?.name ?: 'id'
InvokerHelper.setProperty(target, idProp, InvokerHelper.getProperty(merged, idProp))
if (flush) {
flushSession session
}
return (D) merged
// Sync version after flush so the incremented value is captured
PersistentProperty versionProperty = persistentEntity.version
if (versionProperty != null) {
InvokerHelper.setProperty(target, versionProperty.name, InvokerHelper.getProperty(merged, versionProperty.name))
}
return target
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
(GormStaticApi<D>) HibernateGormEnhancer.findStaticApi(persistentClass, qualifier)
}

@Override
D merge(D d) {
instanceApi.merge(d)
}

@Override
<T> T withNewSession(Closure<T> callable) {
if (persistentEntity.isMultiTenant()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import org.hibernate.MappingException

import org.grails.datastore.mapping.config.Property
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateIdentity
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePropertyIdentity
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty

/**
Expand All @@ -54,7 +54,7 @@ import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentP
@AutoClone
@Builder(builderStrategy = SimpleStrategy, prefix = '')
@CompileStatic
class CompositeIdentity extends Property implements HibernateIdentity {
class HibernateCompositeIdentity extends Property implements HibernatePropertyIdentity {

/**
* The property names that make up the custom identity
Expand All @@ -74,7 +74,7 @@ class CompositeIdentity extends Property implements HibernateIdentity {
* @param naturalIdDef The callable
* @return This id
*/
CompositeIdentity naturalId(@DelegatesTo(NaturalId) Closure naturalIdDef) {
HibernateCompositeIdentity naturalId(@DelegatesTo(NaturalId) Closure naturalIdDef) {
this.natural = new NaturalId()
naturalIdDef.setDelegate(this.natural)
naturalIdDef.setResolveStrategy(Closure.DELEGATE_ONLY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import org.springframework.validation.DataBinder

import org.grails.datastore.mapping.config.Property
import org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsSequenceGeneratorEnum
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateIdentity
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePropertyIdentity

/**
* Defines the identity generation strategy. In the case of a 'composite' identity the properties
Expand All @@ -53,7 +53,7 @@ import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateIdentity
*/
@CompileStatic
@Builder(builderStrategy = SimpleStrategy, prefix = '')
class Identity extends Property implements HibernateIdentity {
class HibernateSimpleIdentity extends Property implements HibernatePropertyIdentity {

/**
* The generator to use
Expand Down Expand Up @@ -92,7 +92,7 @@ class Identity extends Property implements HibernateIdentity {
return useSequence ? GrailsSequenceGeneratorEnum.SEQUENCE_IDENTITY.toString() : GrailsSequenceGeneratorEnum.NATIVE.toString()
}

static String determineGeneratorName(Identity mappedId, boolean useSequence) {
static String determineGeneratorName(HibernateSimpleIdentity mappedId, boolean useSequence) {
if (mappedId != null) {
return mappedId.determineGeneratorName(useSequence)
}
Expand All @@ -104,7 +104,7 @@ class Identity extends Property implements HibernateIdentity {
* @param naturalIdDef The callable
* @return This id
*/
Identity naturalId(@DelegatesTo(NaturalId) Closure naturalIdDef) {
HibernateSimpleIdentity naturalId(@DelegatesTo(NaturalId) Closure naturalIdDef) {
this.natural = new NaturalId()
naturalIdDef.setDelegate(this.natural)
naturalIdDef.setResolveStrategy(Closure.DELEGATE_ONLY)
Expand All @@ -120,8 +120,8 @@ class Identity extends Property implements HibernateIdentity {
* @param config The configuration
* @return The new instance
*/
static Identity configureNew(@DelegatesTo(Identity) Closure config) {
Identity property = new Identity()
static HibernateSimpleIdentity configureNew(@DelegatesTo(HibernateSimpleIdentity) Closure config) {
HibernateSimpleIdentity property = new HibernateSimpleIdentity()
return configureExisting(property, config)
}

Expand All @@ -131,7 +131,7 @@ class Identity extends Property implements HibernateIdentity {
* @param config The configuration
* @return The new instance
*/
static Identity configureExisting(Identity property, Map config) {
static HibernateSimpleIdentity configureExisting(HibernateSimpleIdentity property, Map config) {
DataBinder dataBinder = new DataBinder(property)
dataBinder.bind(new MutablePropertyValues(config))
return property
Expand All @@ -142,7 +142,7 @@ class Identity extends Property implements HibernateIdentity {
* @param config The configuration
* @return The new instance
*/
static Identity configureExisting(Identity property, @DelegatesTo(Identity) Closure config) {
static HibernateSimpleIdentity configureExisting(HibernateSimpleIdentity property, @DelegatesTo(HibernateSimpleIdentity) Closure config) {
config.setDelegate(property)
config.setResolveStrategy(Closure.DELEGATE_ONLY)
config.call()
Expand Down
Loading
Loading