This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch 8.0.x-hibernate7 in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 783beda3f39793fc8925998b6589096e113b6f10 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Thu Feb 12 20:14:56 2026 -0600 Refine GormColumnSnapshotGenerator to explicitly set identifier columns as non-nullable. - Update GormColumnSnapshotGenerator to immediately set column.setNullable(false) upon confirming a column is an identifier. - Ensure all tests in grails-data-hibernate7-dbmigration pass with the refined nullability logic. Migrate database migration module to Hibernate 7 and Liquibase 5.0.1. - Update dbmigration dependencies to use org.liquibase:liquibase-core:5.0.1 and org.liquibase.ext:liquibase-hibernate7:5.0.1-SNAPSHOT. - Implement GormColumnSnapshotGenerator to ensure correct autoIncrement and nullability detection based on GORM metadata. - Fix java.lang.VerifyError in ClosureEventTriggeringInterceptor by relaxing type constraints on Hibernate events. - Add no-arg constructor to GormDatabase for Liquibase SPI compatibility. - Update test expectations in DbmDiffCommandSpec to align with Liquibase 5.x H2 output (INT instead of INTEGER). - Update test domain classes in ApplicationContextDatabaseMigrationCommandSpec to include non-nullable constraints. - Enable mavenLocal() in settings.gradle for local Liquibase artifacts. --- gradle.properties | 1 + .../support/ClosureEventTriggeringInterceptor.java | 7 +- grails-data-hibernate7/dbmigration/build.gradle | 11 +- .../liquibase/GormColumnSnapshotGenerator.groovy | 164 +++++++++++++++++++++ .../liquibase/GormDatabase.groovy | 8 + .../services/liquibase.snapshot.SnapshotGenerator | 1 + ...ationContextDatabaseMigrationCommandSpec.groovy | 4 + .../command/DbmDiffCommandSpec.groovy | 4 +- settings.gradle | 2 +- 9 files changed, 188 insertions(+), 14 deletions(-) diff --git a/gradle.properties b/gradle.properties index 39121b3fe8..d69ecee6a8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -43,6 +43,7 @@ joptSimpleVersion=5.0.4 jspApiVersion=4.0.0 liquibaseHibernate5Version=4.27.0 liquibaseHibernate6Version=4.27.0 +liquibaseHibernate7Version=5.0.1-SNAPSHOT picocliVersion=4.7.6 slf4jVersion=2.0.17 diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java index 7966de5f7e..4f1a4e7da4 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java @@ -32,7 +32,6 @@ import org.hibernate.Hibernate; import org.hibernate.HibernateException; import org.hibernate.event.internal.DefaultMergeEventListener; import org.hibernate.event.internal.DefaultPersistEventListener; -import org.hibernate.event.spi.AbstractEvent; import org.hibernate.event.spi.MergeContext; import org.hibernate.event.spi.MergeEvent; import org.hibernate.event.spi.PersistContext; @@ -341,8 +340,10 @@ public class ClosureEventTriggeringInterceptor extends AbstractClosureEventTrigg publishEvent(hibernateEvent, grailsEvent); } - private void publishEvent(AbstractEvent hibernateEvent, AbstractPersistenceEvent mappingEvent) { - mappingEvent.setNativeEvent(hibernateEvent); + private void publishEvent(Object hibernateEvent, AbstractPersistenceEvent mappingEvent) { + if (hibernateEvent instanceof Serializable) { + mappingEvent.setNativeEvent((Serializable) hibernateEvent); + } if(eventPublisher != null) { eventPublisher.publishEvent(mappingEvent); } diff --git a/grails-data-hibernate7/dbmigration/build.gradle b/grails-data-hibernate7/dbmigration/build.gradle index b0c3636fbc..0313ae8d92 100644 --- a/grails-data-hibernate7/dbmigration/build.gradle +++ b/grails-data-hibernate7/dbmigration/build.gradle @@ -43,8 +43,8 @@ dependencies { exclude group: 'org.liquibase', module: 'liquibase-core' } - implementation("org.liquibase:liquibase-core:$liquibaseHibernate6Version") - implementation("org.liquibase.ext:liquibase-hibernate6:$liquibaseHibernate6Version") { + implementation("org.liquibase:liquibase-core:5.0.1") + implementation("org.liquibase.ext:liquibase-hibernate7:$liquibaseHibernate7Version") { exclude group: 'org.hibernate', module: 'hibernate-core' exclude group: 'org.hibernate', module: 'hibernate-entitymanager' exclude group: 'org.hibernate', module: 'hibernate-envers' @@ -79,12 +79,7 @@ dependencies { constraints { implementation("org.liquibase:liquibase-core") { version { - strictly "$liquibaseHibernate6Version" - } - } - implementation("org.liquibase.ext:liquibase-hibernate6") { - version { - strictly "$liquibaseHibernate6Version" + strictly "5.0.1" } } } diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GormColumnSnapshotGenerator.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GormColumnSnapshotGenerator.groovy new file mode 100644 index 0000000000..1eb8ba32e9 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GormColumnSnapshotGenerator.groovy @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.liquibase + +import groovy.transform.CompileStatic +import liquibase.database.Database +import liquibase.snapshot.DatabaseSnapshot +import liquibase.snapshot.SnapshotGenerator +import liquibase.snapshot.SnapshotGeneratorChain +import liquibase.structure.DatabaseObject +import liquibase.structure.core.Column +import liquibase.structure.core.Relation +import org.grails.datastore.mapping.model.MappingContext +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentEntity +import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentProperty +import org.grails.orm.hibernate.cfg.Mapping +import org.grails.orm.hibernate.cfg.Identity +import org.hibernate.mapping.PersistentClass +import org.hibernate.mapping.RootClass +import org.hibernate.mapping.SimpleValue +import org.hibernate.mapping.Selectable +import org.hibernate.boot.Metadata + +@CompileStatic +class GormColumnSnapshotGenerator implements SnapshotGenerator { + + @Override + int getPriority(Class<? extends DatabaseObject> objectType, Database database) { + if (database instanceof GormDatabase && Column.class.isAssignableFrom(objectType)) { + return 10 + 100 // VERY HIGH PRIORITY + } + return -1 + } + + @Override + Class<? extends DatabaseObject>[] addsTo() { + return [Column] as Class[] + } + + @Override + Class<? extends SnapshotGenerator>[] replaces() { + return [] as Class[] + } + + @Override + public <T extends DatabaseObject> T snapshot(T example, DatabaseSnapshot snapshot, SnapshotGeneratorChain chain) { + T snapshotObject = chain.snapshot(example, snapshot) + + if (snapshotObject instanceof Column && snapshot.getDatabase() instanceof GormDatabase) { + Column column = (Column) snapshotObject + + Relation relation = column.getRelation() + if (relation == null) return snapshotObject + String tableName = relation.getName() + if (tableName == null) return snapshotObject + + GormDatabase gormDb = (GormDatabase) snapshot.getDatabase() + def gormDatastore = gormDb.getGormDatastore() + if (gormDatastore == null) return snapshotObject + + MappingContext mappingContext = gormDatastore.getMappingContext() + Metadata metadata = gormDb.getMetadata() + if (metadata == null) return snapshotObject + + for (PersistentClass pc : metadata.getEntityBindings()) { + if (pc instanceof RootClass) { + RootClass root = (RootClass) pc + if (tableName.equalsIgnoreCase(root.getTable()?.getName())) { + org.hibernate.mapping.Column hibernateColumn = null + for (org.hibernate.mapping.Column hc : root.getTable().getColumns()) { + if (hc.getName().equalsIgnoreCase(column.getName())) { + hibernateColumn = hc + break + } + } + + if (hibernateColumn != null) { + PersistentEntity entity = mappingContext.getPersistentEntity(pc.getClassName() ?: pc.getEntityName()) + if (entity instanceof GrailsHibernatePersistentEntity) { + GrailsHibernatePersistentEntity gpe = (GrailsHibernatePersistentEntity) entity + + // 1. Check if it is an ID column + boolean isIdColumn = false + if (root.getIdentifier() instanceof SimpleValue) { + SimpleValue sv = (SimpleValue) root.getIdentifier() + for (Selectable s : sv.getColumns()) { + if (s instanceof org.hibernate.mapping.Column) { + if (s.getName().equalsIgnoreCase(column.getName())) { + isIdColumn = true + break + } + } + } + } + + if (isIdColumn) { + // Always set identifiers as non-nullable + column.setNullable(false) + + Mapping m = gpe.getMappedForm() + Object idMapping = m.getIdentity() + if (idMapping instanceof Identity) { + Identity identity = (Identity) idMapping + boolean useSequence = m.isTablePerConcreteClass() + String strategy = identity.determineGeneratorName(useSequence) + if ("identity" == strategy || "native" == strategy || "sequence-identity" == strategy) { + column.setAutoIncrementInformation(new Column.AutoIncrementInformation()) + } + } + } else { + // 2. Fix nullability for non-ID columns using GORM metadata + if (column.isNullable() == null || column.isNullable()) { + boolean gormNullable = true + for (PersistentProperty prop : gpe.getPersistentProperties()) { + String propColumnName = null + if (prop instanceof GrailsHibernatePersistentProperty) { + propColumnName = ((GrailsHibernatePersistentProperty) prop).getMappedColumnName() + } + if (propColumnName == null) { + // Default naming convention + propColumnName = prop.getName() + if (prop instanceof org.grails.datastore.mapping.model.types.Association) { + propColumnName += "_id" + } + } + + if (column.getName().equalsIgnoreCase(propColumnName)) { + gormNullable = prop.isNullable() + break + } + } + + if (!gormNullable) { + column.setNullable(false) + } + } + } + } + } + } + } + } + } + return snapshotObject + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GormDatabase.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GormDatabase.groovy index b1315943bb..acc9aaf141 100644 --- a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GormDatabase.groovy +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GormDatabase.groovy @@ -43,14 +43,18 @@ class GormDatabase extends HibernateDatabase { private Dialect dialect private Metadata metadata + private HibernateDatastore gormDatastore DatabaseConnection connection + + GormDatabase() { } GormDatabase(Dialect dialect, ServiceRegistry serviceRegistry, HibernateDatastore hibernateDatastore) { this.dialect = dialect this.metadata = hibernateDatastore.getMetadata() + this.gormDatastore = hibernateDatastore SnapshotControl snapshotControl = new SnapshotControl(this, null, null) GormDatabase database = this OfflineConnection connection = new OfflineConnection('offline:gorm', null) { @@ -74,6 +78,10 @@ class GormDatabase extends HibernateDatabase { metadata } + HibernateDatastore getGormDatastore() { + gormDatastore + } + @Override protected void configureSources(MetadataSources sources) throws DatabaseException { //no op diff --git a/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.snapshot.SnapshotGenerator b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.snapshot.SnapshotGenerator new file mode 100644 index 0000000000..407f191698 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.snapshot.SnapshotGenerator @@ -0,0 +1 @@ +org.grails.plugins.databasemigration.liquibase.GormColumnSnapshotGenerator diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/ApplicationContextDatabaseMigrationCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/ApplicationContextDatabaseMigrationCommandSpec.groovy index 9199398b00..5e4f61d8c3 100644 --- a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/ApplicationContextDatabaseMigrationCommandSpec.groovy +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/ApplicationContextDatabaseMigrationCommandSpec.groovy @@ -118,6 +118,10 @@ abstract class ApplicationContextDatabaseMigrationCommandSpec extends DatabaseMi class Book { String title Author author + static belongsTo = [author: Author] + static constraints = { + author nullable: false + } } @Entity diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmDiffCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmDiffCommandSpec.groovy index 043b350d46..f48303ae68 100644 --- a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmDiffCommandSpec.groovy +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmDiffCommandSpec.groovy @@ -77,7 +77,7 @@ databaseChangeLog = \\{ changeSet\\(author: ".+?", id: ".+?"\\) \\{ addColumn\\(tableName: "BOOK"\\) \\{ - column\\(name: "PRICE", type: "INTEGER"\\) \\{ + column\\(name: "PRICE", type: "INT"\\) \\{ constraints\\(nullable: "false"\\) \\} \\} @@ -111,7 +111,7 @@ databaseChangeLog = \\{ changeSet\\(author: ".+?", id: ".+?"\\) \\{ addColumn\\(tableName: "BOOK"\\) \\{ - column\\(name: "PRICE", type: "INTEGER"\\) \\{ + column\\(name: "PRICE", type: "INT"\\) \\{ constraints\\(nullable: "false"\\) \\} \\} diff --git a/settings.gradle b/settings.gradle index a21c55043d..78b389765a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -535,7 +535,7 @@ for (String pattern in DirectoryScanner.defaultExcludes) { dependencyResolutionManagement { repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS repositories { - // mavenLocal() // Keep, this will be uncommented and used by CI (groovy-joint-workflow) + mavenLocal() maven { url = 'https://repo.grails.org/grails/restricted' } maven { url = 'https://repository.apache.org/content/groups/snapshots'
