From 41c71bfed7d8eb74bd0574ed7bd2552d055dcf46 Mon Sep 17 00:00:00 2001 From: Emmanuel Duchastenier Date: Tue, 23 Mar 2021 17:37:35 +0100 Subject: [PATCH] HHH-13779 (5.4) - Foreign key schema migrator should be case-insensitive This avoids re-creating existing foreign keys with a different name, after migrating from Hibernate 4 to Hibernate 5 (as implicit naming convention has changed). Actually, some RDBMS allow it (PostgreSQL, MySQL, MS SQL Server, ...) and duplicate the same key, whereas others (Oracle, ...) do not allow it and Schema update fails. This fix ignores the case of the table and column name when checking if a equivalent Foreign Key already exists (whatever its name) Closes https://hibernate.atlassian.net/browse/HHH-13779 (cherry picked from commit 0b819863f2cee41c7067d7e3656356f0b1221ef1) --- .../internal/AbstractSchemaMigrator.java | 21 +++--- .../internal/AbstractSchemaMigratorTest.java | 73 +++++++++++++++++++ 2 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/tool/schema/internal/AbstractSchemaMigratorTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/AbstractSchemaMigrator.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/AbstractSchemaMigrator.java index e38b3034b0..0f58cfee50 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/AbstractSchemaMigrator.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/AbstractSchemaMigrator.java @@ -463,15 +463,7 @@ public abstract class AbstractSchemaMigrator implements SchemaMigrator { * Find existing keys based on referencing column and referencedTable. "referencedColumnName" is not checked * because that always is the primary key of the "referencedTable". */ - Predicate mappingPredicate = m -> { - String existingReferencingColumn = m.getReferencingColumnMetadata().getColumnIdentifier().getText(); - String existingReferencedTable = m.getReferencedColumnMetadata().getContainingTableInformation().getName().getTableName().getCanonicalName(); - return referencingColumn.equals( existingReferencingColumn ) && referencedTable.equals( existingReferencedTable ); - }; - Stream keyStream = StreamSupport.stream( tableInformation.getForeignKeys().spliterator(), false ); - Stream mappingStream = keyStream.flatMap( k -> StreamSupport.stream( k.getColumnReferenceMappings().spliterator(), false ) ); - boolean found = mappingStream.anyMatch( mappingPredicate ); - if ( found ) { + if (equivalentForeignKeyExistsInDatabase(tableInformation, referencingColumn, referencedTable)) { return true; } @@ -480,6 +472,17 @@ public abstract class AbstractSchemaMigrator implements SchemaMigrator { return tableInformation.getForeignKey( Identifier.toIdentifier( foreignKey.getName() ) ) != null; } + boolean equivalentForeignKeyExistsInDatabase(TableInformation tableInformation, String referencingColumn, String referencedTable) { + Predicate mappingPredicate = m -> { + String existingReferencingColumn = m.getReferencingColumnMetadata().getColumnIdentifier().getText(); + String existingReferencedTable = m.getReferencedColumnMetadata().getContainingTableInformation().getName().getTableName().getCanonicalName(); + return referencingColumn.equalsIgnoreCase( existingReferencingColumn ) && referencedTable.equalsIgnoreCase( existingReferencedTable ); + }; + Stream keyStream = StreamSupport.stream( tableInformation.getForeignKeys().spliterator(), false ); + Stream mappingStream = keyStream.flatMap( k -> StreamSupport.stream( k.getColumnReferenceMappings().spliterator(), false ) ); + return mappingStream.anyMatch( mappingPredicate ); + } + protected void checkExportIdentifier(Exportable exportable, Set exportIdentifiers) { final String exportIdentifier = exportable.getExportIdentifier(); if ( exportIdentifiers.contains( exportIdentifier ) ) { diff --git a/hibernate-core/src/test/java/org/hibernate/tool/schema/internal/AbstractSchemaMigratorTest.java b/hibernate-core/src/test/java/org/hibernate/tool/schema/internal/AbstractSchemaMigratorTest.java new file mode 100644 index 0000000000..8635fb788a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/tool/schema/internal/AbstractSchemaMigratorTest.java @@ -0,0 +1,73 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.tool.schema.internal; + +import java.util.ArrayList; +import java.util.Set; + +import org.hibernate.boot.Metadata; +import org.hibernate.boot.model.TruthValue; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.relational.Namespace; +import org.hibernate.boot.model.relational.QualifiedTableName; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.internal.Formatter; +import org.hibernate.testing.TestForIssue; +import org.hibernate.tool.schema.extract.internal.ColumnInformationImpl; +import org.hibernate.tool.schema.extract.internal.ForeignKeyInformationImpl; +import org.hibernate.tool.schema.extract.spi.DatabaseInformation; +import org.hibernate.tool.schema.extract.spi.ForeignKeyInformation; +import org.hibernate.tool.schema.extract.spi.NameSpaceTablesInformation; +import org.hibernate.tool.schema.extract.spi.TableInformation; +import org.hibernate.tool.schema.internal.exec.GenerationTarget; +import org.hibernate.tool.schema.spi.ExecutionOptions; +import org.junit.Test; + +import static java.util.Collections.singletonList; +import static org.hamcrest.core.Is.is; +import static org.hibernate.boot.model.naming.Identifier.toIdentifier; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +/** + * @author Emmanuel Duchastenier + */ +public class AbstractSchemaMigratorTest { + + @Test + @TestForIssue(jiraKey = "HHH-13779") + public void testForeignKeyPreExistenceDetectionIgnoresCaseForTableAndColumnName() { + final AbstractSchemaMigrator schemaMigrator = new AbstractSchemaMigrator(null, null) { + @Override + protected NameSpaceTablesInformation performTablesMigration(Metadata metadata, DatabaseInformation existingDatabase, ExecutionOptions options, Dialect dialect, Formatter formatter, Set exportIdentifiers, boolean tryToCreateCatalogs, boolean tryToCreateSchemas, Set exportedCatalogs, Namespace namespace, GenerationTarget[] targets) { return null; } + }; + + final TableInformation existingTableInformation = mock(TableInformation.class); + final ArrayList columnReferenceMappings = new ArrayList<>(); + + final TableInformation destinationTableInformation = mock(TableInformation.class); + doReturn(new QualifiedTableName(toIdentifier("catalog"), toIdentifier("schema"), + toIdentifier("referenced_table"))) // Table name is lower case + .when(destinationTableInformation).getName(); + columnReferenceMappings.add(new ForeignKeyInformationImpl.ColumnReferenceMappingImpl( + new ColumnInformationImpl(null, toIdentifier("referencing_column"), // column name is lower case + 0, "typeName", 255, 0, TruthValue.TRUE), + new ColumnInformationImpl(destinationTableInformation, null, 1, "typeName", 0, 0, TruthValue.TRUE))); + doReturn(singletonList(new ForeignKeyInformationImpl(toIdentifier("FKp8mpamfw2inhj88hwhty1eipm"), columnReferenceMappings))) + .when(existingTableInformation).getForeignKeys(); + + final boolean existInDatabase = schemaMigrator.equivalentForeignKeyExistsInDatabase( + existingTableInformation, + "REFERENCING_COLUMN", "REFERENCED_TABLE"); // Table and column names are UPPER-case here, to prove the test + + assertThat("Expected ForeignKey pre-existence check to be case-insensitive", + existInDatabase, + is(true)); + } + +} \ No newline at end of file