Copilot commented on code in PR #16497:
URL: https://github.com/apache/grails-core/pull/16497#discussion_r4175510220
##########
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:
`HibernateIndexSnapshotGenerator` already sets index uniqueness
(`setUnique()` at line 71). Unconditionally removing this difference also hides
genuine `true`/`false` mismatches, such as a mapped non-unique index compared
with an existing unique index, leaving an unwanted uniqueness restriction in
place. Suppress only missing metadata. Update the suppression test to use
realistic null/Boolean values, and add a Hibernate-involved test that preserves
a concrete Boolean mismatch.
##########
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:
`SequenceStyleGenerator` can use a table-backed structure, for example with
`force_table_use=true` or a dialect without sequence support. This branch
records that table as a `Sequence`, introducing fictitious sequence changes
into the migration diff. Require `isPhysicalSequence()` before adding it, and
add a table-backed generator as a negative test.
##########
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:
Lowercasing the deduplication key collapses distinct quoted identifiers such
as `"Foo"` and `"foo"`. If both sequences are discovered through generators,
the second is skipped and is missing from the snapshot. Use the same
quote-aware identifier key in both discovery paths, preserving case for quoted
names, and cover two generator-owned sequences that differ only in quoted case.
##########
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:
`@NativeGenerator` declares generation through `@IdGeneratorType` and does
not require `@GeneratedValue`. This guard rejects a valid `@Id
@NativeGenerator` mapping without `@GeneratedValue`, so column snapshots omit
identity metadata and generator sequence discovery skips it. Recognize the
native annotation as generation intent too, and test both identity and sequence
snapshots without the redundant `@GeneratedValue` in `NativeGenEntity`.
##########
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:
When a non-PostgreSQL dialect selects `SEQUENCE` for a native generator,
`handleSequenceGenerator()` returns `database.supportsAutoIncrement()`, which
is always true in `HibernateDatabase`. The caller then marks a
sequence-generated column as an identity column, producing incorrect
auto-increment migration metadata. Return false for the native `SEQUENCE`
strategy, including when its delegate cannot be resolved, while preserving
PostgreSQL default handling. Add a non-PostgreSQL sequence-strategy regression
test.
##########
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();
Review Comment:
`render()` includes catalog/schema qualification and quoting, but the
namespace loop uses the unqualified identifier text. A sequence already
recorded as `item_seq` can therefore be added again as `app.item_seq`, with the
qualifier incorrectly embedded in Liquibase's sequence name. Use the object
identifier text consistently with the existing namespace snapshot, and test
qualified and quoted sequence names.
--
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]