HHH-18221 Fix for Incomplete list of existing foreign keys - DatabaseMetaData.crossReferences(...) not used

This commit is contained in:
Vladimír Kuruc 2024-06-03 14:30:56 +02:00 committed by Christian Beikov
parent 1e70c51b56
commit 753076bb40
4 changed files with 195 additions and 42 deletions

View File

@ -573,6 +573,26 @@ public class InformixDialect extends Dialect {
return BeforeUseAction.CREATE; return BeforeUseAction.CREATE;
} }
@Override
public String[] getCreateSchemaCommand(String schemaName) {
return new String[] { "create schema authorization " + schemaName };
}
@Override
public String[] getDropSchemaCommand(String schemaName) {
return new String[] { "" };
}
@Override
public boolean useCrossReferenceForeignKeys(){
return true;
}
@Override
public String getCrossReferenceParentTableFilter(){
return "%";
}
@Override @Override
public UniqueDelegate getUniqueDelegate() { public UniqueDelegate getUniqueDelegate() {
return uniqueDelegate; return uniqueDelegate;

View File

@ -2637,6 +2637,23 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun
+ " " + foreignKeyDefinition; + " " + foreignKeyDefinition;
} }
/**
* Does the dialect also need cross-references to get a complete
* list of foreign keys?
*/
public boolean useCrossReferenceForeignKeys(){
return false;
}
/**
* Some dialects require a not null primaryTable filter.
* Sometimes a wildcard entry is sufficient for the like condition.
* @return
*/
public String getCrossReferenceParentTableFilter(){
return null;
}
/** /**
* The syntax used to add a primary key constraint to a table. * The syntax used to add a primary key constraint to a table.
* *

View File

@ -1264,6 +1264,92 @@ public abstract class AbstractInformationExtractorImpl implements InformationExt
ExtractionContext.ResultSetProcessor<T> processor ExtractionContext.ResultSetProcessor<T> processor
) throws SQLException; ) throws SQLException;
/**
* Must do the following:
* <ol>
* <li>
* obtain a {@link ResultSet} containing a row for each foreign key
* column making up a foreign key for any existing
* foreignCatalog/foreignSchema/foreignTable combination as specified by
* parameters described below.
* The {@link ResultSet} must contain the following, consistent
* with the corresponding columns returned by {@link DatabaseMetaData#getCrossReference}:
* <ul>
* <li>
* column label {@link #getResultSetForeignKeyLabel} -
* foreign key name (may be null)
* </li>
* <li>
* column label {@link #getResultSetPrimaryKeyCatalogLabel} -
* primary key table catalog being imported (may be null)
* </li>
* <li>
* column label {@link #getResultSetPrimaryKeySchemaLabel} -
* primary key table schema being imported (may be null)
* </li>
* <li>
* column label {@link #getResultSetPrimaryKeyTableLabel} -
* primary key table name being imported
* </li>
* <li>
* column label {@link #getResultSetForeignKeyColumnNameLabel} -
* foreign key column name
* </li>
* <li>
* column label {@link #getResultSetPrimaryKeyColumnNameLabel} -
* primary key column name being imported
* </li>
* </ul>
* The ResultSet must be ordered by the primary key
* foreignCatalog/foreignSchema/foreignTable and column position within the key.
* </li>
* <li> execute {@code processor.process( resultSet )};</li>
* <li>
* release resources whether {@code processor.process( resultSet )}
* executes successfully or not.
* </li>
* </ol>
* <p>
* The {@code parentCatalog}, {@code parentSchema}, {@code parentTable},
* {@code foreignCatalog}, {@code foreignSchema}, {@code foreignTable}
* parameters are as specified by {@link DatabaseMetaData#getCrossReference(
* String, String, String, String, String, String)}
* and are copied here:
*
* @param parentCatalog a catalog name; must match the catalog name
* as it is stored in the database; "" retrieves those without a
* catalog; {@code null} means drop catalog name from the selection criteria
* @param parentSchema a schema name; must match the schema name as
* it is stored in the database; "" retrieves those without a schema;
* {@code null} means drop schema name from the selection criteria
* @param parentTable the name of the table that exports the key; must match
* the table name as it is stored in the database
* @param foreignCatalog a catalog name; must match the catalog name as
* it is stored in the database; "" retrieves those without a
* catalog; {@code null} means drop catalog name from the selection criteria
* @param foreignSchema a schema name; must match the schema name as it
* is stored in the database; "" retrieves those without a schema;
* {@code null} means drop schema name from the selection criteria
* @param foreignTable the name of the table that imports the key; must match
* the table name as it is stored in the database
* @param processor - the provided ResultSetProcessor.
* @param <T> - defined by {@code processor}
* @return - defined by {@code processor}
* @throws SQLException - if a database error occurs
* @see #processImportedKeysResultSet(String, String, String,
* ExtractionContext.ResultSetProcessor)
*/
protected abstract <T> T processCrossReferenceResultSet(
String parentCatalog,
String parentSchema,
String parentTable,
String foreignCatalog,
String foreignSchema,
String foreignTable,
ExtractionContext.ResultSetProcessor<T> processor
) throws SQLException;
@Override @Override
public Iterable<ForeignKeyInformation> getForeignKeys(TableInformation tableInformation) { public Iterable<ForeignKeyInformation> getForeignKeys(TableInformation tableInformation) {
final Map<Identifier, ForeignKeyBuilder> fkBuilders = new HashMap<>(); final Map<Identifier, ForeignKeyBuilder> fkBuilders = new HashMap<>();
@ -1289,52 +1375,62 @@ public abstract class AbstractInformationExtractorImpl implements InformationExt
} }
try { try {
ExtractionContext.ResultSetProcessor<Void> processor = resultSet -> {
while ( resultSet.next() ) {
// IMPL NOTE : The builder is mainly used to collect the column reference mappings
final Identifier fkIdentifier = DatabaseIdentifier.toIdentifier(
resultSet.getString( getResultSetForeignKeyLabel() )
);
ForeignKeyBuilder fkBuilder = fkBuilders.get( fkIdentifier );
if ( fkBuilder == null ) {
fkBuilder = generateForeignKeyBuilder( fkIdentifier );
fkBuilders.put( fkIdentifier, fkBuilder );
}
final QualifiedTableName incomingPkTableName = extractPrimaryKeyTableName( resultSet );
final TableInformation pkTableInformation = extractionContext.getDatabaseObjectAccess()
.locateTableInformation( incomingPkTableName );
if ( pkTableInformation == null ) {
// the assumption here is that we have not seen this table already based on fully-qualified name
// during previous step of building all table metadata so most likely this is
// not a match based solely on schema/catalog and that another row in this result set
// should match.
continue;
}
final Identifier fkColumnIdentifier = DatabaseIdentifier.toIdentifier(
resultSet.getString( getResultSetForeignKeyColumnNameLabel() )
);
final Identifier pkColumnIdentifier = DatabaseIdentifier.toIdentifier(
resultSet.getString( getResultSetPrimaryKeyColumnNameLabel() )
);
fkBuilder.addColumnMapping(
tableInformation.getColumn( fkColumnIdentifier ),
pkTableInformation.getColumn( pkColumnIdentifier )
);
}
return null;
};
processImportedKeysResultSet( processImportedKeysResultSet(
catalogFilter, catalogFilter,
schemaFilter, schemaFilter,
tableInformation.getName().getTableName().getText(), tableInformation.getName().getTableName().getText(),
resultSet -> { processor);
// todo : need to account for getCrossReference() as well... final Dialect dialect = extractionContext.getJdbcEnvironment().getDialect();
if (dialect.useCrossReferenceForeignKeys()) {
while ( resultSet.next() ) { processCrossReferenceResultSet(
// IMPL NOTE : The builder is mainly used to collect the column reference mappings null,
final Identifier fkIdentifier = DatabaseIdentifier.toIdentifier( null,
resultSet.getString( getResultSetForeignKeyLabel() ) dialect.getCrossReferenceParentTableFilter(),
); catalogFilter,
ForeignKeyBuilder fkBuilder = fkBuilders.get( fkIdentifier ); schemaFilter,
if ( fkBuilder == null ) { tableInformation.getName().getTableName().getText(),
fkBuilder = generateForeignKeyBuilder( fkIdentifier ); processor
fkBuilders.put( fkIdentifier, fkBuilder ); );
} }
final QualifiedTableName incomingPkTableName = extractPrimaryKeyTableName( resultSet );
final TableInformation pkTableInformation = extractionContext.getDatabaseObjectAccess()
.locateTableInformation( incomingPkTableName );
if ( pkTableInformation == null ) {
// the assumption here is that we have not seen this table already based on fully-qualified name
// during previous step of building all table metadata so most likely this is
// not a match based solely on schema/catalog and that another row in this result set
// should match.
continue;
}
final Identifier fkColumnIdentifier = DatabaseIdentifier.toIdentifier(
resultSet.getString( getResultSetForeignKeyColumnNameLabel() )
);
final Identifier pkColumnIdentifier = DatabaseIdentifier.toIdentifier(
resultSet.getString( getResultSetPrimaryKeyColumnNameLabel() )
);
fkBuilder.addColumnMapping(
tableInformation.getColumn( fkColumnIdentifier ),
pkTableInformation.getColumn( pkColumnIdentifier )
);
}
return null;
}
);
} }
catch (SQLException e) { catch (SQLException e) {
throw convertSQLException( throw convertSQLException(

View File

@ -134,6 +134,26 @@ public class InformationExtractorJdbcDatabaseMetaDataImpl extends AbstractInform
} }
} }
@Override
protected <T> T processCrossReferenceResultSet(
String parentCatalog,
String parentSchema,
String parentTable,
String foreignCatalog,
String foreignSchema,
String foreignTable,
ExtractionContext.ResultSetProcessor<T> processor) throws SQLException {
try (ResultSet resultSet = getExtractionContext().getJdbcDatabaseMetaData().getCrossReference(
parentCatalog,
parentSchema,
parentTable,
foreignCatalog,
foreignSchema,
foreignTable) ) {
return processor.process( resultSet );
}
}
protected void addColumns(TableInformation tableInformation) { protected void addColumns(TableInformation tableInformation) {
final Dialect dialect = getExtractionContext().getJdbcEnvironment().getDialect(); final Dialect dialect = getExtractionContext().getJdbcEnvironment().getDialect();