jamesfredley commented on code in PR #16497:
URL: https://github.com/apache/grails-core/pull/16497#discussion_r4179358137


##########
grails-data-hibernate7/dbmigration-core/src/main/java/liquibase/ext/hibernate/snapshot/HibernateColumnSnapshotGenerator.java:
##########
@@ -267,6 +268,41 @@ public Class<? extends SnapshotGenerator>[] replaces() {
         return new Class[] 
{liquibase.snapshot.jvm.ColumnSnapshotGenerator.class};
     }
 
+    private boolean handleSequenceGenerator(
+            org.hibernate.id.enhanced.SequenceStyleGenerator seqGen,
+            Dialect dialect,
+            HibernateDatabase database,
+            Column column,
+            org.hibernate.mapping.Table hibernateTable,
+            org.hibernate.mapping.Column hibernateColumn) {
+        if (PostgreSQLDialect.class.isAssignableFrom(dialect.getClass())) {
+            String sequenceName = resolveSequenceName(seqGen, hibernateTable, 
hibernateColumn);
+            column.setDefaultValue(new DatabaseFunction("nextval('" + 
sequenceName + "'::regclass)"));
+            return false;
+        }
+        return database.supportsAutoIncrement();
+    }
+
+    private boolean handleNativeGenerator(
+            org.hibernate.id.NativeGenerator nativeGen,
+            Dialect dialect,
+            HibernateDatabase database,
+            Column column,
+            org.hibernate.mapping.Table hibernateTable,
+            org.hibernate.mapping.Column hibernateColumn) {
+        return switch (nativeGen.getGenerationType()) {
+            case IDENTITY -> true;
+            case SEQUENCE -> {
+                var delegate = 
IdentifierGeneratorSupport.nativeDelegate(nativeGen);
+                if (delegate instanceof 
org.hibernate.id.enhanced.SequenceStyleGenerator seqGen) {
+                    yield handleSequenceGenerator(seqGen, dialect, database, 
column, hibernateTable, hibernateColumn);
+                }
+                yield database.supportsAutoIncrement();
+            }

Review Comment:
   Fixed in 2a4a40a746. Sequence and table generators are not marked 
auto-increment. Only an identity generator is. HSQL AUTO and the Spring bean 
native mapping are no longer auto-increment. H2 classic native still is, 
because that dialect's native strategy is identity. Oracle native sequence 
coverage is in IdentifierGeneratorSnapshotTest.



##########
grails-data-hibernate7/dbmigration-core/src/main/java/liquibase/ext/hibernate/diff/HibernateChangedIndexChangeGenerator.java:
##########
@@ -0,0 +1,41 @@
+package liquibase.ext.hibernate.diff;
+
+import liquibase.change.Change;
+import liquibase.database.Database;
+import liquibase.diff.ObjectDifferences;
+import liquibase.diff.output.DiffOutputControl;
+import liquibase.diff.output.changelog.ChangeGeneratorChain;
+import liquibase.ext.hibernate.database.HibernateDatabase;
+import liquibase.structure.DatabaseObject;
+import liquibase.structure.core.Index;
+
+/**
+ * Hibernate does not know every index attribute ({@code unique}, {@code 
using}), so those differences are
+ * suppressed to prevent needless drop and recreate changes on every diff.
+ */
+public class HibernateChangedIndexChangeGenerator
+        extends 
liquibase.diff.output.changelog.core.ChangedIndexChangeGenerator {
+
+    @Override
+    public int getPriority(Class<? extends DatabaseObject> objectType, 
Database database) {
+        return Index.class.isAssignableFrom(objectType) ? PRIORITY_ADDITIONAL 
: PRIORITY_NONE;
+    }
+
+    @Override
+    public Change[] fixChanged(
+            DatabaseObject changedObject,
+            ObjectDifferences differences,
+            DiffOutputControl control,
+            Database referenceDatabase,
+            Database comparisonDatabase,
+            ChangeGeneratorChain chain) {
+        if (referenceDatabase instanceof HibernateDatabase || 
comparisonDatabase instanceof HibernateDatabase) {
+            differences.removeDifference("unique");

Review Comment:
   Fixed in 2a4a40a746. Index uniqueness now comes from 
hibernateIndex.isUnique(), not Column.isUnique() and not a fabricated false for 
composite indexes. A concrete Boolean mismatch is still kept. Unknown 
uniqueness and the using attribute are still suppressed when a 
HibernateDatabase is in the diff.



##########
grails-data-hibernate7/dbmigration-core/src/main/java/liquibase/ext/hibernate/snapshot/HibernateSequenceSnapshotGenerator.java:
##########
@@ -36,15 +48,69 @@ protected void addTo(DatabaseObject foundObject, 
DatabaseSnapshot snapshot)
 
         if (foundObject instanceof Schema schema) {
             HibernateDatabase database = (HibernateDatabase) 
snapshot.getDatabase();
+            Set<String> addedSequences = new HashSet<>();
+
             for (org.hibernate.boot.model.relational.Namespace namespace :
                     database.getMetadata().getDatabase().getNamespaces()) {
                 for (org.hibernate.boot.model.relational.Sequence sequence : 
namespace.getSequences()) {
+                    String name = 
sequence.getName().getSequenceName().getText();
                     schema.addDatabaseObject(new Sequence()
-                            
.setName(sequence.getName().getSequenceName().getText())
+                            .setName(name)
                             .setSchema(schema)
                             
.setStartValue(BigInteger.valueOf(sequence.getInitialValue()))
                             
.setIncrementBy(BigInteger.valueOf(sequence.getIncrementSize())));
+                    addedSequences.add(name.toLowerCase(Locale.ROOT));
+                }
+            }
+
+            addGeneratorSequences(database, schema, addedSequences);
+        }
+    }
+
+    private void addGeneratorSequences(HibernateDatabase database, Schema 
schema, Set<String> addedSequences) {
+        MetadataImplementor metadata = (MetadataImplementor) 
database.getMetadata();
+        var dialect = database.getDialect();
+
+        for (PersistentClass entityBinding : metadata.getEntityBindings()) {
+            if (!(entityBinding instanceof RootClass rootClass) ||
+                    !(rootClass.getIdentifier() instanceof SimpleValue 
simpleValue) ||
+                    
!IdentifierGeneratorSupport.hasGenerationIntent(simpleValue)) {
+                continue;
+            }
+
+            try {
+                var generator = simpleValue.createGenerator(
+                        dialect,
+                        rootClass,
+                        rootClass.getIdentifierProperty(),
+                        
IdentifierGeneratorSupport.createGeneratorSettings(simpleValue));
+
+                SequenceStyleGenerator seqGen = null;
+                // NativeGenerator may wrap a SequenceStyleGenerator delegate 
depending on the dialect.
+                if (generator instanceof NativeGenerator nativeGen) {
+                    if (IdentifierGeneratorSupport.nativeDelegate(nativeGen) 
instanceof SequenceStyleGenerator s) {
+                        seqGen = s;
+                    }
+                } else if (generator instanceof SequenceStyleGenerator s) {
+                    seqGen = s;
+                }
+
+                if (seqGen != null) {
+                    var structure = seqGen.getDatabaseStructure();
+                    if (structure != null && structure.getPhysicalName() != 
null) {

Review Comment:
   Fixed in 2a4a40a746. Table-backed SequenceStyleGenerator structures are not 
recorded as sequences. MySQL emulation of the auction package and an explicit 
force_table_use mapping are both covered.



##########
grails-data-hibernate7/dbmigration-core/src/main/java/liquibase/ext/hibernate/snapshot/HibernateSequenceSnapshotGenerator.java:
##########
@@ -36,15 +48,69 @@ protected void addTo(DatabaseObject foundObject, 
DatabaseSnapshot snapshot)
 
         if (foundObject instanceof Schema schema) {
             HibernateDatabase database = (HibernateDatabase) 
snapshot.getDatabase();
+            Set<String> addedSequences = new HashSet<>();
+
             for (org.hibernate.boot.model.relational.Namespace namespace :
                     database.getMetadata().getDatabase().getNamespaces()) {
                 for (org.hibernate.boot.model.relational.Sequence sequence : 
namespace.getSequences()) {
+                    String name = 
sequence.getName().getSequenceName().getText();
                     schema.addDatabaseObject(new Sequence()
-                            
.setName(sequence.getName().getSequenceName().getText())
+                            .setName(name)
                             .setSchema(schema)
                             
.setStartValue(BigInteger.valueOf(sequence.getInitialValue()))
                             
.setIncrementBy(BigInteger.valueOf(sequence.getIncrementSize())));
+                    addedSequences.add(name.toLowerCase(Locale.ROOT));
+                }
+            }
+
+            addGeneratorSequences(database, schema, addedSequences);
+        }
+    }
+
+    private void addGeneratorSequences(HibernateDatabase database, Schema 
schema, Set<String> addedSequences) {
+        MetadataImplementor metadata = (MetadataImplementor) 
database.getMetadata();
+        var dialect = database.getDialect();
+
+        for (PersistentClass entityBinding : metadata.getEntityBindings()) {
+            if (!(entityBinding instanceof RootClass rootClass) ||
+                    !(rootClass.getIdentifier() instanceof SimpleValue 
simpleValue) ||
+                    
!IdentifierGeneratorSupport.hasGenerationIntent(simpleValue)) {
+                continue;
+            }
+
+            try {
+                var generator = simpleValue.createGenerator(
+                        dialect,
+                        rootClass,
+                        rootClass.getIdentifierProperty(),
+                        
IdentifierGeneratorSupport.createGeneratorSettings(simpleValue));
+
+                SequenceStyleGenerator seqGen = null;
+                // NativeGenerator may wrap a SequenceStyleGenerator delegate 
depending on the dialect.
+                if (generator instanceof NativeGenerator nativeGen) {
+                    if (IdentifierGeneratorSupport.nativeDelegate(nativeGen) 
instanceof SequenceStyleGenerator s) {
+                        seqGen = s;
+                    }
+                } else if (generator instanceof SequenceStyleGenerator s) {
+                    seqGen = s;
+                }
+
+                if (seqGen != null) {
+                    var structure = seqGen.getDatabaseStructure();
+                    if (structure != null && structure.getPhysicalName() != 
null) {
+                        String name = structure.getPhysicalName().render();
+                        if (addedSequences.add(name.toLowerCase(Locale.ROOT))) 
{

Review Comment:
   Not fully fixed, so I am leaving this thread open.
   
   The dedup key uses Identifier.getCanonicalName(), which preserves quoted 
case and folds unquoted names. Liquibase 4.27 Sequence.equals and hashCode 
still compare names case-insensitively, so a public snapshot cannot retain both 
Foo and foo even when our key treats them as distinct. I did not add a snapshot 
assertion that cannot pass.



##########
grails-data-hibernate7/dbmigration-core/src/main/java/liquibase/ext/hibernate/snapshot/IdentifierGeneratorSupport.java:
##########
@@ -0,0 +1,68 @@
+package liquibase.ext.hibernate.snapshot;
+
+import liquibase.Scope;
+import org.hibernate.boot.model.relational.SqlStringGenerationContext;
+import 
org.hibernate.boot.model.relational.internal.SqlStringGenerationContextImpl;
+import org.hibernate.generator.Generator;
+import org.hibernate.id.NativeGenerator;
+import org.hibernate.mapping.GeneratorSettings;
+import org.hibernate.mapping.SimpleValue;
+
+/**
+ * Shared helpers for the snapshot generators that inspect identifier 
generators.
+ */
+final class IdentifierGeneratorSupport {
+
+    private IdentifierGeneratorSupport() {
+    }
+
+    /**
+     * For annotation-based entities an identifier without {@code 
@GeneratedValue} is application-assigned, so no
+     * generator applies. XML-mapped entities have no member details and 
declare their generator in the hbm.xml
+     * mapping, so they always count as having generation intent.
+     */
+    static boolean hasGenerationIntent(SimpleValue simpleValue) {
+        var memberDetails = simpleValue.getMemberDetails();
+        return memberDetails == null ||
+                
memberDetails.hasDirectAnnotationUsage(jakarta.persistence.GeneratedValue.class);
+    }

Review Comment:
   Fixed in 2a4a40a746. hasGenerationIntent uses GeneratorCreator.isAssigned() 
when a creator is present, and a direct @NativeGenerator still counts as 
generation intent. NativeGenEntity no longer carries a redundant 
@GeneratedValue, and both native tests pass. @SnowflakeId without 
@GeneratedValue is still not treated as a database-generated id.



##########
grails-data-hibernate7/dbmigration-core/src/main/java/liquibase/ext/hibernate/snapshot/HibernateColumnSnapshotGenerator.java:
##########
@@ -267,6 +268,41 @@ public Class<? extends SnapshotGenerator>[] replaces() {
         return new Class[] 
{liquibase.snapshot.jvm.ColumnSnapshotGenerator.class};
     }
 
+    private boolean handleSequenceGenerator(
+            org.hibernate.id.enhanced.SequenceStyleGenerator seqGen,
+            Dialect dialect,
+            HibernateDatabase database,
+            Column column,
+            org.hibernate.mapping.Table hibernateTable,
+            org.hibernate.mapping.Column hibernateColumn) {
+        if (PostgreSQLDialect.class.isAssignableFrom(dialect.getClass())) {
+            String sequenceName = resolveSequenceName(seqGen, hibernateTable, 
hibernateColumn);
+            column.setDefaultValue(new DatabaseFunction("nextval('" + 
sequenceName + "'::regclass)"));
+            return false;
+        }
+        return database.supportsAutoIncrement();
+    }
+
+    private boolean handleNativeGenerator(
+            org.hibernate.id.NativeGenerator nativeGen,
+            Dialect dialect,
+            HibernateDatabase database,
+            Column column,
+            org.hibernate.mapping.Table hibernateTable,
+            org.hibernate.mapping.Column hibernateColumn) {
+        return switch (nativeGen.getGenerationType()) {
+            case IDENTITY -> true;
+            case SEQUENCE -> {
+                var delegate = 
IdentifierGeneratorSupport.nativeDelegate(nativeGen);
+                if (delegate instanceof 
org.hibernate.id.enhanced.SequenceStyleGenerator seqGen) {

Review Comment:
   Fixed in 2a4a40a746. An unconfigured GrailsNativeGenerator no longer 
dereferences a null DatabaseStructure, and it does not invent a nextval 
default. GormIdentifierSnapshot16497Spec snapshots native, sequence, and 
identity ids through GormDatabase on PostgreSQL: native is auto-increment with 
no nextval default, sequence keeps its nextval default, and identity is 
auto-increment.



##########
grails-data-hibernate7/dbmigration-core/src/main/java/liquibase/ext/hibernate/snapshot/IdentifierGeneratorSupport.java:
##########
@@ -0,0 +1,68 @@
+package liquibase.ext.hibernate.snapshot;
+
+import liquibase.Scope;
+import org.hibernate.boot.model.relational.SqlStringGenerationContext;
+import 
org.hibernate.boot.model.relational.internal.SqlStringGenerationContextImpl;
+import org.hibernate.generator.Generator;
+import org.hibernate.id.NativeGenerator;
+import org.hibernate.mapping.GeneratorSettings;
+import org.hibernate.mapping.SimpleValue;
+
+/**
+ * Shared helpers for the snapshot generators that inspect identifier 
generators.
+ */
+final class IdentifierGeneratorSupport {
+
+    private IdentifierGeneratorSupport() {
+    }
+
+    /**
+     * For annotation-based entities an identifier without {@code 
@GeneratedValue} is application-assigned, so no
+     * generator applies. XML-mapped entities have no member details and 
declare their generator in the hbm.xml
+     * mapping, so they always count as having generation intent.
+     */
+    static boolean hasGenerationIntent(SimpleValue simpleValue) {
+        var memberDetails = simpleValue.getMemberDetails();
+        return memberDetails == null ||
+                
memberDetails.hasDirectAnnotationUsage(jakarta.persistence.GeneratedValue.class);

Review Comment:
   Fixed in 2a4a40a746. hasGenerationIntent uses GeneratorCreator.isAssigned() 
when a creator is present, and a direct @NativeGenerator still counts as 
generation intent. NativeGenEntity no longer carries a redundant 
@GeneratedValue, and both native tests pass. @SnowflakeId without 
@GeneratedValue is still not treated as a database-generated id.



##########
grails-data-hibernate7/dbmigration-core/src/main/java/liquibase/ext/hibernate/snapshot/HibernateSequenceSnapshotGenerator.java:
##########
@@ -36,15 +48,69 @@ protected void addTo(DatabaseObject foundObject, 
DatabaseSnapshot snapshot)
 
         if (foundObject instanceof Schema schema) {
             HibernateDatabase database = (HibernateDatabase) 
snapshot.getDatabase();
+            Set<String> addedSequences = new HashSet<>();
+
             for (org.hibernate.boot.model.relational.Namespace namespace :
                     database.getMetadata().getDatabase().getNamespaces()) {
                 for (org.hibernate.boot.model.relational.Sequence sequence : 
namespace.getSequences()) {
+                    String name = 
sequence.getName().getSequenceName().getText();
                     schema.addDatabaseObject(new Sequence()
-                            
.setName(sequence.getName().getSequenceName().getText())
+                            .setName(name)
                             .setSchema(schema)
                             
.setStartValue(BigInteger.valueOf(sequence.getInitialValue()))
                             
.setIncrementBy(BigInteger.valueOf(sequence.getIncrementSize())));
+                    addedSequences.add(name.toLowerCase(Locale.ROOT));
+                }
+            }
+
+            addGeneratorSequences(database, schema, addedSequences);
+        }
+    }
+
+    private void addGeneratorSequences(HibernateDatabase database, Schema 
schema, Set<String> addedSequences) {
+        MetadataImplementor metadata = (MetadataImplementor) 
database.getMetadata();
+        var dialect = database.getDialect();
+
+        for (PersistentClass entityBinding : metadata.getEntityBindings()) {
+            if (!(entityBinding instanceof RootClass rootClass) ||
+                    !(rootClass.getIdentifier() instanceof SimpleValue 
simpleValue) ||
+                    
!IdentifierGeneratorSupport.hasGenerationIntent(simpleValue)) {
+                continue;
+            }
+
+            try {
+                var generator = simpleValue.createGenerator(
+                        dialect,
+                        rootClass,
+                        rootClass.getIdentifierProperty(),
+                        
IdentifierGeneratorSupport.createGeneratorSettings(simpleValue));
+
+                SequenceStyleGenerator seqGen = null;
+                // NativeGenerator may wrap a SequenceStyleGenerator delegate 
depending on the dialect.
+                if (generator instanceof NativeGenerator nativeGen) {
+                    if (IdentifierGeneratorSupport.nativeDelegate(nativeGen) 
instanceof SequenceStyleGenerator s) {
+                        seqGen = s;
+                    }
+                } else if (generator instanceof SequenceStyleGenerator s) {
+                    seqGen = s;
+                }
+
+                if (seqGen != null) {
+                    var structure = seqGen.getDatabaseStructure();
+                    if (structure != null && structure.getPhysicalName() != 
null) {

Review Comment:
   Fixed in 2a4a40a746. Table-backed SequenceStyleGenerator structures are not 
recorded as sequences. MySQL emulation of the auction package and an explicit 
force_table_use mapping are both covered.



##########
grails-data-hibernate7/dbmigration-core/src/main/java/liquibase/ext/hibernate/diff/HibernateChangedIndexChangeGenerator.java:
##########
@@ -0,0 +1,41 @@
+package liquibase.ext.hibernate.diff;
+
+import liquibase.change.Change;
+import liquibase.database.Database;
+import liquibase.diff.ObjectDifferences;
+import liquibase.diff.output.DiffOutputControl;
+import liquibase.diff.output.changelog.ChangeGeneratorChain;
+import liquibase.ext.hibernate.database.HibernateDatabase;
+import liquibase.structure.DatabaseObject;
+import liquibase.structure.core.Index;
+
+/**
+ * Hibernate does not know every index attribute ({@code unique}, {@code 
using}), so those differences are
+ * suppressed to prevent needless drop and recreate changes on every diff.
+ */
+public class HibernateChangedIndexChangeGenerator
+        extends 
liquibase.diff.output.changelog.core.ChangedIndexChangeGenerator {
+
+    @Override
+    public int getPriority(Class<? extends DatabaseObject> objectType, 
Database database) {
+        return Index.class.isAssignableFrom(objectType) ? PRIORITY_ADDITIONAL 
: PRIORITY_NONE;
+    }
+
+    @Override
+    public Change[] fixChanged(
+            DatabaseObject changedObject,
+            ObjectDifferences differences,
+            DiffOutputControl control,
+            Database referenceDatabase,
+            Database comparisonDatabase,
+            ChangeGeneratorChain chain) {
+        if (referenceDatabase instanceof HibernateDatabase || 
comparisonDatabase instanceof HibernateDatabase) {
+            differences.removeDifference("unique");

Review Comment:
   Fixed in 2a4a40a746. Index uniqueness now comes from 
hibernateIndex.isUnique(), not Column.isUnique() and not a fabricated false for 
composite indexes. A concrete Boolean mismatch is still kept. Unknown 
uniqueness and the using attribute are still suppressed when a 
HibernateDatabase is in the diff.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to