diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index 9c27922fe18d..83b691ec7557 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java @@ -70,7 +70,10 @@ import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.tool.schema.extract.internal.InformationExtractorPostgreSQLImpl; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.InformationExtractor; import org.hibernate.type.JavaObjectType; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.ClobJdbcType; @@ -1218,4 +1221,8 @@ public boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } + @Override + public InformationExtractor getInformationExtractor(ExtractionContext extractionContext) { + return new InformationExtractorPostgreSQLImpl( extractionContext ); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/GaussDBDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/GaussDBDialect.java index eb9f7f6cf90a..ec1882995618 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/GaussDBDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/GaussDBDialect.java @@ -75,7 +75,10 @@ import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.internal.OptionalTableUpdate; import org.hibernate.sql.model.jdbc.OptionalTableUpdateOperation; +import org.hibernate.tool.schema.extract.internal.InformationExtractorPostgreSQLImpl; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.InformationExtractor; import org.hibernate.tool.schema.internal.StandardTableExporter; import org.hibernate.tool.schema.spi.Exporter; import org.hibernate.type.JavaObjectType; @@ -1377,4 +1380,9 @@ public boolean supportsFromClauseInUpdate() { public boolean supportsBindingNullSqlTypeForSetNull() { return true; } + + @Override + public InformationExtractor getInformationExtractor(ExtractionContext extractionContext) { + return new InformationExtractorPostgreSQLImpl( extractionContext ); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index ef978f66baf7..c7a0a2d09167 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -101,8 +101,11 @@ import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.tool.schema.extract.internal.InformationExtractorOracleImpl; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorOracleDatabaseImpl; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.InformationExtractor; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.tool.schema.internal.StandardTableExporter; import org.hibernate.tool.schema.spi.Exporter; @@ -1770,4 +1773,8 @@ public boolean supportsRowValueConstructorSyntaxInInSubQuery() { return getVersion().isSameOrAfter( 9 ); } + @Override + public InformationExtractor getInformationExtractor(ExtractionContext extractionContext) { + return new InformationExtractorOracleImpl( extractionContext ); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index b9b74920d784..09fd9f8dd613 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java @@ -94,7 +94,10 @@ import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.tool.schema.extract.internal.InformationExtractorPostgreSQLImpl; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.InformationExtractor; import org.hibernate.tool.schema.internal.StandardTableExporter; import org.hibernate.tool.schema.spi.Exporter; import org.hibernate.type.JavaObjectType; @@ -1649,4 +1652,9 @@ public boolean supportsRecursiveSearchClause() { return getVersion().isSameOrAfter( 14 ); } + @Override + public InformationExtractor getInformationExtractor(ExtractionContext extractionContext) { + return new InformationExtractorPostgreSQLImpl( extractionContext ); + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index ab37154d0024..c40a5cdad6d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -62,7 +62,10 @@ import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.tool.schema.extract.internal.InformationExtractorPostgreSQLImpl; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.InformationExtractor; import org.hibernate.type.JavaObjectType; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.ClobJdbcType; @@ -1189,4 +1192,9 @@ public String getReadLockString(int timeout) { public String getReadLockString(String aliases, int timeout) { return withTimeout( " for share of " + aliases, timeout ); } + + @Override + public InformationExtractor getInformationExtractor(ExtractionContext extractionContext) { + return new InformationExtractorPostgreSQLImpl( extractionContext ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index e3145abc05c5..1fd8ed286548 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -129,9 +129,12 @@ import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.internal.OptionalTableUpdate; import org.hibernate.sql.model.jdbc.OptionalTableUpdateOperation; +import org.hibernate.tool.schema.extract.internal.InformationExtractorJdbcDatabaseMetaDataImpl; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLegacyImpl; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.InformationExtractor; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.tool.schema.internal.HibernateSchemaManagementTool; import org.hibernate.tool.schema.internal.StandardAuxiliaryDatabaseObjectExporter; @@ -2173,6 +2176,16 @@ public SequenceInformationExtractor getSequenceInformationExtractor() { : SequenceInformationExtractorLegacyImpl.INSTANCE; } + /** + * A {@link InformationExtractor} which is able to extract + * table, primary key, foreign key, index information etc. via JDBC. + * + * @since 7.2 + */ + public InformationExtractor getInformationExtractor(ExtractionContext extractionContext) { + return new InformationExtractorJdbcDatabaseMetaDataImpl( extractionContext ); + } + // GUID support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index 32069be58970..9112d2a3de1b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -68,6 +68,9 @@ import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.tool.schema.extract.internal.InformationExtractorMySQLImpl; +import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.InformationExtractor; import org.hibernate.type.BasicTypeRegistry; import org.hibernate.type.NullType; import org.hibernate.type.SqlTypes; @@ -1666,4 +1669,8 @@ public MutationOperation createOptionalTableUpdateOperation(EntityMutationTarget return super.createOptionalTableUpdateOperation( mutationTarget, optionalTableUpdate, factory ); } + @Override + public InformationExtractor getInformationExtractor(ExtractionContext extractionContext) { + return new InformationExtractorMySQLImpl( extractionContext ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 8348512e1349..af640780f4a7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -87,8 +87,11 @@ import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.tool.schema.extract.internal.InformationExtractorOracleImpl; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorOracleDatabaseImpl; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.InformationExtractor; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.tool.schema.internal.StandardTableExporter; import org.hibernate.tool.schema.spi.Exporter; @@ -1874,4 +1877,9 @@ public boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } + @Override + public InformationExtractor getInformationExtractor(ExtractionContext extractionContext) { + return new InformationExtractorOracleImpl( extractionContext ); + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index e2185157b3a5..67097ab6d2bb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -85,7 +85,10 @@ import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.tool.schema.extract.internal.InformationExtractorPostgreSQLImpl; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.InformationExtractor; import org.hibernate.tool.schema.internal.StandardTableExporter; import org.hibernate.tool.schema.spi.Exporter; import org.hibernate.type.JavaObjectType; @@ -1648,4 +1651,8 @@ public boolean supportsRecursiveSearchClause() { return getVersion().isSameOrAfter( 14 ); } + @Override + public InformationExtractor getInformationExtractor(ExtractionContext extractionContext) { + return new InformationExtractorPostgreSQLImpl( extractionContext ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/AbstractInformationExtractorImpl.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/AbstractInformationExtractorImpl.java index f5662c877990..809e30d7a1f7 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/AbstractInformationExtractorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/AbstractInformationExtractorImpl.java @@ -16,8 +16,10 @@ import java.util.Objects; import java.util.StringTokenizer; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.JDBCException; 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.DB2Dialect; import org.hibernate.dialect.Dialect; @@ -32,6 +34,9 @@ import org.hibernate.tool.schema.extract.spi.ForeignKeyInformation; import org.hibernate.tool.schema.extract.spi.IndexInformation; import org.hibernate.tool.schema.extract.spi.InformationExtractor; +import org.hibernate.tool.schema.extract.spi.NameSpaceForeignKeysInformation; +import org.hibernate.tool.schema.extract.spi.NameSpaceIndexesInformation; +import org.hibernate.tool.schema.extract.spi.NameSpacePrimaryKeysInformation; import org.hibernate.tool.schema.extract.spi.NameSpaceTablesInformation; import org.hibernate.tool.schema.extract.spi.PrimaryKeyInformation; import org.hibernate.tool.schema.extract.spi.SchemaExtractionException; @@ -163,6 +168,15 @@ protected String getResultSetPrimaryKeySchemaLabel() { protected String getResultSetPrimaryKeyTableLabel() { return "PKTABLE_NAME"; } + protected String getResultSetForeignKeyCatalogLabel() { + return "FKTABLE_CAT"; + } + protected String getResultSetForeignKeySchemaLabel() { + return "FKTABLE_SCHEM"; + } + protected String getResultSetForeignKeyTableLabel() { + return "FKTABLE_NAME"; + } protected String getResultSetColumnNameLabel() { return "COLUMN_NAME"; } @@ -882,8 +896,20 @@ protected abstract T processPrimaryKeysResultSet( ExtractionContext.ResultSetProcessor processor) throws SQLException; + protected abstract T processPrimaryKeysResultSet( + String catalogFilter, + String schemaFilter, + @Nullable String tableName, + ExtractionContext.ResultSetProcessor processor) + throws SQLException; + @Override - public PrimaryKeyInformation getPrimaryKey(TableInformationImpl tableInformation) { + public @Nullable PrimaryKeyInformation getPrimaryKey(TableInformation tableInformation) { + final var databaseObjectAccess = extractionContext.getDatabaseObjectAccess(); + if ( databaseObjectAccess.isCaching() && supportsBulkPrimaryKeyRetrieval() ) { + return databaseObjectAccess.locatePrimaryKeyInformation( tableInformation.getName() ); + } + final var tableName = tableInformation.getName(); final Identifier catalog = tableName.getCatalogName(); final Identifier schema = tableName.getSchemaName(); @@ -950,6 +976,89 @@ private PrimaryKeyInformation extractPrimaryKeyInformation(TableInformation tabl } } + @Override + public NameSpacePrimaryKeysInformation getPrimaryKeys(Identifier catalog, Identifier schema) { + if ( !supportsBulkPrimaryKeyRetrieval() ) { + throw new UnsupportedOperationException( "Database doesn't support extracting all primary keys at once" ); + } + else { + try { + return processPrimaryKeysResultSet( + catalog == null ? "" : catalog.getText(), + schema == null ? "" : schema.getText(), + (String) null, + this::extractNameSpacePrimaryKeysInformation + ); + } + catch (SQLException e) { + throw convertSQLException( e, + "Error while reading primary key meta data for namespace " + + new Namespace.Name( catalog, schema ) ); + } + } + } + + private TableInformation getTableInformation( + @Nullable String catalogName, + @Nullable String schemaName, + @Nullable String tableName) { + final var qualifiedTableName = new QualifiedTableName( + toIdentifier( catalogName ), + toIdentifier( schemaName ), + toIdentifier( tableName ) + ); + final var tableInformation = + extractionContext.getDatabaseObjectAccess().locateTableInformation( qualifiedTableName ); + if ( tableInformation == null ) { + throw new SchemaExtractionException( "Could not locate table information for " + qualifiedTableName ); + } + return tableInformation; + } + + protected NameSpacePrimaryKeysInformation extractNameSpacePrimaryKeysInformation(ResultSet resultSet) + throws SQLException { + final var primaryKeysInformation = new NameSpacePrimaryKeysInformation( getIdentifierHelper() ); + + while ( resultSet.next() ) { + final String currentTableName = resultSet.getString( getResultSetPrimaryKeyTableLabel() ); + final String currentPkName = resultSet.getString( getResultSetPrimaryKeyNameLabel() ); + final Identifier currentPrimaryKeyIdentifier = + currentPkName == null ? null : toIdentifier( currentPkName ); + final TableInformation tableInformation = getTableInformation( + resultSet.getString( getResultSetPrimaryKeyCatalogLabel() ), + resultSet.getString( getResultSetPrimaryKeySchemaLabel() ), + currentTableName + ); + PrimaryKeyInformation primaryKeyInformation = + primaryKeysInformation.getPrimaryKeyInformation( currentTableName ); + final List columns; + if ( primaryKeyInformation != null ) { + if ( !Objects.equals( primaryKeyInformation.getPrimaryKeyIdentifier(), currentPrimaryKeyIdentifier ) ) { + throw new SchemaExtractionException( "Encountered primary keys differing name on table " + + currentTableName ); + } + columns = (List) primaryKeyInformation.getColumns(); + } + else { + columns = new ArrayList<>(); + primaryKeyInformation = new PrimaryKeyInformationImpl( currentPrimaryKeyIdentifier, columns ); + primaryKeysInformation.addPrimaryKeyInformation( tableInformation, primaryKeyInformation ); + } + + final int columnPosition = resultSet.getInt( getResultSetColumnPositionColumn() ); + final int index = columnPosition - 1; + // Fill up the array list with nulls up to the desired index, because some JDBC drivers don't return results ordered by column position + while ( columns.size() <= index ) { + columns.add( null ); + } + final Identifier columnIdentifier = + toIdentifier( resultSet.getString( getResultSetColumnNameLabel() ) ); + columns.set( index, tableInformation.getColumn( columnIdentifier ) ); + } + primaryKeysInformation.validate(); + return primaryKeysInformation; + } + /** * Must do the following: *
    @@ -1022,7 +1131,7 @@ private PrimaryKeyInformation extractPrimaryKeyInformation(TableInformation tabl protected abstract T processIndexInfoResultSet( String catalog, String schema, - String table, + @Nullable String table, boolean unique, boolean approximate, ExtractionContext.ResultSetProcessor processor) @@ -1030,6 +1139,11 @@ protected abstract T processIndexInfoResultSet( @Override public Iterable getIndexes(TableInformation tableInformation) { + final var databaseObjectAccess = extractionContext.getDatabaseObjectAccess(); + if ( databaseObjectAccess.isCaching() && supportsBulkIndexRetrieval() ) { + return databaseObjectAccess.locateIndexesInformation( tableInformation.getName() ); + } + final var tableName = tableInformation.getName(); final Identifier catalog = tableName.getCatalogName(); final Identifier schema = tableName.getSchemaName(); @@ -1093,6 +1207,79 @@ private static IndexInformationImpl.Builder indexInformationBuilder( return builder; } + @Override + public NameSpaceIndexesInformation getIndexes(Identifier catalog, Identifier schema) { + if ( !supportsBulkIndexRetrieval() ) { + throw new UnsupportedOperationException( "Database doesn't support extracting all indexes at once" ); + } + else { + try { + return processIndexInfoResultSet( + catalog == null ? "" : catalog.getText(), + schema == null ? "" : schema.getText(), + null, + false, + true, + this::extractNameSpaceIndexesInformation + ); + } + catch (SQLException e) { + throw convertSQLException( e, + "Error while reading index information for namespace " + + new Namespace.Name( catalog, schema ) ); + } + } + } + + protected NameSpaceIndexesInformation extractNameSpaceIndexesInformation(ResultSet resultSet) + throws SQLException { + final var indexesInformation = new NameSpaceIndexesInformation( getIdentifierHelper() ); + + while ( resultSet.next() ) { + if ( resultSet.getShort( getResultSetIndexTypeLabel() ) + != DatabaseMetaData.tableIndexStatistic ) { + final TableInformation tableInformation = getTableInformation( + resultSet.getString( getResultSetCatalogLabel() ), + resultSet.getString( getResultSetSchemaLabel() ), + resultSet.getString( getResultSetTableNameLabel() ) + ); + final Identifier indexIdentifier = + toIdentifier( resultSet.getString( getResultSetIndexNameLabel() ) ); + final var index = getOrCreateIndexInformation( indexesInformation, indexIdentifier, tableInformation ); + final Identifier columnIdentifier = + toIdentifier( resultSet.getString( getResultSetColumnNameLabel() ) ); + final var columnInformation = tableInformation.getColumn( columnIdentifier ); + if ( columnInformation == null ) { + // See HHH-10191: this may happen when dealing with Oracle/PostgreSQL function indexes + CORE_LOGGER.logCannotLocateIndexColumnInformation( + columnIdentifier.getText(), + indexIdentifier.getText() + ); + } + index.getIndexedColumns().add( columnInformation ); + } + } + return indexesInformation; + } + + private IndexInformation getOrCreateIndexInformation( + NameSpaceIndexesInformation indexesInformation, + Identifier indexIdentifier, + TableInformation tableInformation) { + final List indexes = + indexesInformation.getIndexesInformation( tableInformation.getName().getTableName().getText() ); + if ( indexes != null ) { + for ( IndexInformation index : indexes ) { + if ( indexIdentifier.equals( index.getIndexIdentifier() ) ) { + return index; + } + } + } + final var indexInformation = new IndexInformationImpl( indexIdentifier, new ArrayList<>() ); + indexesInformation.addIndexInformation( tableInformation, indexInformation ); + return indexInformation; + } + /** * Must do the following: *
      @@ -1162,7 +1349,7 @@ private static IndexInformationImpl.Builder indexInformationBuilder( protected abstract T processImportedKeysResultSet( String catalog, String schema, - String table, + @Nullable String table, ExtractionContext.ResultSetProcessor processor) throws SQLException; @@ -1254,6 +1441,11 @@ protected abstract T processCrossReferenceResultSet( @Override public Iterable getForeignKeys(TableInformation tableInformation) { + final var databaseObjectAccess = extractionContext.getDatabaseObjectAccess(); + if ( databaseObjectAccess.isCaching() && supportsBulkForeignKeyRetrieval() ) { + return databaseObjectAccess.locateForeignKeyInformation( tableInformation.getName() ); + } + final var tableName = tableInformation.getName(); final Identifier catalog = tableName.getCatalogName(); final Identifier schema = tableName.getSchemaName(); @@ -1296,6 +1488,82 @@ public Iterable getForeignKeys(TableInformation tableInfo return foreignKeys; } + @Override + public NameSpaceForeignKeysInformation getForeignKeys(Identifier catalog, Identifier schema) { + if ( !supportsBulkForeignKeyRetrieval() ) { + throw new UnsupportedOperationException( "Database doesn't support extracting all foreign keys at once" ); + } + else { + try { + return processImportedKeysResultSet( + catalog == null ? "" : catalog.getText(), + schema == null ? "" : schema.getText(), + null, + this::extractNameSpaceForeignKeysInformation + ); + } + catch (SQLException e) { + throw convertSQLException( e, + "Error while reading foreign key information for namespace " + + new Namespace.Name( catalog, schema ) ); + } + } + } + + protected NameSpaceForeignKeysInformation extractNameSpaceForeignKeysInformation(ResultSet resultSet) + throws SQLException { + final var foreignKeysInformation = new NameSpaceForeignKeysInformation( getIdentifierHelper() ); + + while ( resultSet.next() ) { + final TableInformation tableInformation = getTableInformation( + resultSet.getString( getResultSetForeignKeyCatalogLabel() ), + resultSet.getString( getResultSetForeignKeySchemaLabel() ), + resultSet.getString( getResultSetForeignKeyTableLabel() ) + ); + final Identifier foreignKeyIdentifier = + toIdentifier( resultSet.getString( getResultSetForeignKeyLabel() ) ); + final var foreignKey = getOrCreateForeignKeyInformation( foreignKeysInformation, foreignKeyIdentifier, tableInformation ); + final var primaryKeyTableInformation = + extractionContext.getDatabaseObjectAccess() + .locateTableInformation( extractPrimaryKeyTableName( resultSet ) ); + if ( primaryKeyTableInformation != 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. + final Identifier foreignKeyColumnIdentifier = + toIdentifier( resultSet.getString( getResultSetForeignKeyColumnNameLabel() ) ); + final Identifier pkColumnIdentifier = + toIdentifier( resultSet.getString( getResultSetPrimaryKeyColumnNameLabel() ) ); + ((List) foreignKey.getColumnReferenceMappings()).add( + new ColumnReferenceMappingImpl( + tableInformation.getColumn( foreignKeyColumnIdentifier ), + primaryKeyTableInformation.getColumn( pkColumnIdentifier ) + ) + ); + } + } + return foreignKeysInformation; + } + + private ForeignKeyInformation getOrCreateForeignKeyInformation( + NameSpaceForeignKeysInformation foreignKeysInformation, + Identifier foreignKeyIdentifier, + TableInformation tableInformation) { + final List foreignKeys = + foreignKeysInformation.getForeignKeysInformation( tableInformation.getName().getTableName().getText() ); + if ( foreignKeys != null ) { + for ( ForeignKeyInformation foreignKey : foreignKeys ) { + if ( foreignKeyIdentifier.equals( foreignKey.getForeignKeyIdentifier() ) ) { + return foreignKey; + } + } + } + final var foreignKeyInformation = new ForeignKeyInformationImpl( foreignKeyIdentifier, new ArrayList<>() ); + foreignKeysInformation.addForeignKeyInformation( tableInformation, foreignKeyInformation ); + return foreignKeyInformation; + } + private void process( TableInformation tableInformation, ResultSet resultSet, @@ -1387,4 +1655,19 @@ private QualifiedTableName extractTableName(ResultSet resultSet) throws SQLExcep ); } + @Override + public boolean supportsBulkPrimaryKeyRetrieval() { + return false; + } + + @Override + public boolean supportsBulkForeignKeyRetrieval() { + return false; + } + + @Override + public boolean supportsBulkIndexRetrieval() { + return false; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/CachingDatabaseInformationImpl.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/CachingDatabaseInformationImpl.java new file mode 100644 index 000000000000..4d8e8c4449cd --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/CachingDatabaseInformationImpl.java @@ -0,0 +1,109 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.tool.schema.extract.internal; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.boot.model.relational.Namespace; +import org.hibernate.boot.model.relational.QualifiedTableName; +import org.hibernate.boot.model.relational.SqlStringGenerationContext; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.resource.transaction.spi.DdlTransactionIsolator; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.tool.schema.extract.spi.ForeignKeyInformation; +import org.hibernate.tool.schema.extract.spi.IndexInformation; +import org.hibernate.tool.schema.extract.spi.NameSpaceForeignKeysInformation; +import org.hibernate.tool.schema.extract.spi.NameSpaceIndexesInformation; +import org.hibernate.tool.schema.extract.spi.NameSpacePrimaryKeysInformation; +import org.hibernate.tool.schema.extract.spi.NameSpaceTablesInformation; +import org.hibernate.tool.schema.extract.spi.PrimaryKeyInformation; +import org.hibernate.tool.schema.extract.spi.TableInformation; +import org.hibernate.tool.schema.spi.SchemaManagementTool; + +import java.sql.SQLException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @since 7.2 + */ +public class CachingDatabaseInformationImpl extends DatabaseInformationImpl { + + private final Map namespaceCacheEntries = new HashMap<>(); + + public CachingDatabaseInformationImpl( + ServiceRegistry serviceRegistry, + JdbcEnvironment jdbcEnvironment, + SqlStringGenerationContext context, + DdlTransactionIsolator ddlTransactionIsolator, + SchemaManagementTool tool) throws SQLException { + super( serviceRegistry, jdbcEnvironment, context, ddlTransactionIsolator, tool ); + } + + @Override + public @Nullable TableInformation locateTableInformation(QualifiedTableName tableName) { + final var namespace = new Namespace.Name( tableName.getCatalogName(), tableName.getSchemaName() ); + final var entry = namespaceCacheEntries.computeIfAbsent( namespace, k -> new NamespaceCacheEntry() ); + NameSpaceTablesInformation nameSpaceTablesInformation = entry.tableInformation; + if ( nameSpaceTablesInformation == null ) { + nameSpaceTablesInformation = extractor.getTables( namespace.catalog(), namespace.schema() ); + entry.tableInformation = nameSpaceTablesInformation; + } + return nameSpaceTablesInformation.getTableInformation( tableName.getTableName().getText() ); + } + + @Override + public @Nullable PrimaryKeyInformation locatePrimaryKeyInformation(QualifiedTableName tableName) { + final var namespace = new Namespace.Name( tableName.getCatalogName(), tableName.getSchemaName() ); + final var entry = namespaceCacheEntries.computeIfAbsent( namespace, k -> new NamespaceCacheEntry() ); + NameSpacePrimaryKeysInformation nameSpaceTablesInformation = entry.primaryKeysInformation; + if ( nameSpaceTablesInformation == null ) { + nameSpaceTablesInformation = extractor.getPrimaryKeys( namespace.catalog(), namespace.schema() ); + entry.primaryKeysInformation = nameSpaceTablesInformation; + } + return nameSpaceTablesInformation.getPrimaryKeyInformation( tableName.getTableName().getText() ); + } + + @Override + public Iterable locateForeignKeyInformation(QualifiedTableName tableName) { + final var namespace = new Namespace.Name( tableName.getCatalogName(), tableName.getSchemaName() ); + final var entry = namespaceCacheEntries.computeIfAbsent( namespace, k -> new NamespaceCacheEntry() ); + NameSpaceForeignKeysInformation nameSpaceTablesInformation = entry.foreignKeysInformation; + if ( nameSpaceTablesInformation == null ) { + nameSpaceTablesInformation = extractor.getForeignKeys( namespace.catalog(), namespace.schema() ); + entry.foreignKeysInformation = nameSpaceTablesInformation; + } + final List foreignKeysInformation = + nameSpaceTablesInformation.getForeignKeysInformation( tableName.getTableName().getText() ); + return foreignKeysInformation == null ? Collections.emptyList() : foreignKeysInformation; + } + + @Override + public Iterable locateIndexesInformation(QualifiedTableName tableName) { + final var namespace = new Namespace.Name( tableName.getCatalogName(), tableName.getSchemaName() ); + final var entry = namespaceCacheEntries.computeIfAbsent( namespace, k -> new NamespaceCacheEntry() ); + NameSpaceIndexesInformation nameSpaceTablesInformation = entry.indexesInformation; + if ( nameSpaceTablesInformation == null ) { + nameSpaceTablesInformation = extractor.getIndexes( namespace.catalog(), namespace.schema() ); + entry.indexesInformation = nameSpaceTablesInformation; + } + final List indexesInformation = + nameSpaceTablesInformation.getIndexesInformation( tableName.getTableName().getText() ); + return indexesInformation == null ? Collections.emptyList() : indexesInformation; + } + + @Override + public boolean isCaching() { + return true; + } + + private static class NamespaceCacheEntry { + NameSpaceTablesInformation tableInformation; + NameSpacePrimaryKeysInformation primaryKeysInformation; + NameSpaceForeignKeysInformation foreignKeysInformation; + NameSpaceIndexesInformation indexesInformation; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/DatabaseInformationImpl.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/DatabaseInformationImpl.java index 1f39f9a72643..6b6d296706cf 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/DatabaseInformationImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/DatabaseInformationImpl.java @@ -8,6 +8,7 @@ import java.util.HashMap; import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.boot.model.naming.Identifier; import org.hibernate.boot.model.relational.Namespace; import org.hibernate.boot.model.relational.QualifiedSequenceName; @@ -18,8 +19,12 @@ import org.hibernate.service.ServiceRegistry; import org.hibernate.tool.schema.extract.spi.DatabaseInformation; import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.ForeignKeyInformation; +import org.hibernate.tool.schema.extract.spi.IndexInformation; import org.hibernate.tool.schema.extract.spi.InformationExtractor; import org.hibernate.tool.schema.extract.spi.NameSpaceTablesInformation; +import org.hibernate.tool.schema.extract.spi.PrimaryKeyInformation; +import org.hibernate.tool.schema.extract.spi.SchemaExtractionException; import org.hibernate.tool.schema.extract.spi.SequenceInformation; import org.hibernate.tool.schema.extract.spi.TableInformation; import org.hibernate.tool.schema.spi.SchemaManagementTool; @@ -29,10 +34,10 @@ */ public class DatabaseInformationImpl implements DatabaseInformation, ExtractionContext.DatabaseObjectAccess { - private final JdbcEnvironment jdbcEnvironment; - private final SqlStringGenerationContext context; - private final ExtractionContext extractionContext; - private final InformationExtractor extractor; + protected final JdbcEnvironment jdbcEnvironment; + protected final SqlStringGenerationContext context; + protected final ExtractionContext extractionContext; + protected final InformationExtractor extractor; private final Map sequenceInformationMap = new HashMap<>(); @@ -144,7 +149,7 @@ public void cleanup() { } @Override - public TableInformation locateTableInformation(QualifiedTableName tableName) { + public @Nullable TableInformation locateTableInformation(QualifiedTableName tableName) { return getTableInformation( tableName ); } @@ -156,4 +161,32 @@ public SequenceInformation locateSequenceInformation(QualifiedSequenceName seque } return sequenceInformationMap.get( sequenceName ); } + + @Override + public PrimaryKeyInformation locatePrimaryKeyInformation(QualifiedTableName tableName) { + return extractor.getPrimaryKey( locateNonNullTableInformation( tableName ) ); + } + + @Override + public Iterable locateForeignKeyInformation(QualifiedTableName tableName) { + return extractor.getForeignKeys( locateNonNullTableInformation( tableName ) ); + } + + @Override + public Iterable locateIndexesInformation(QualifiedTableName tableName) { + return extractor.getIndexes( locateNonNullTableInformation( tableName ) ); + } + + private TableInformation locateNonNullTableInformation(QualifiedTableName tableName) { + final TableInformation tableInformation = locateTableInformation( tableName ); + if ( tableInformation == null ) { + throw new SchemaExtractionException( "Could not locate table information for " + tableName ); + } + return tableInformation; + } + + @Override + public boolean isCaching() { + return false; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorJdbcDatabaseMetaDataImpl.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorJdbcDatabaseMetaDataImpl.java index e9a7c816a70f..ff47a9ac0537 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorJdbcDatabaseMetaDataImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorJdbcDatabaseMetaDataImpl.java @@ -9,6 +9,7 @@ import java.sql.SQLException; import java.util.StringTokenizer; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.boot.model.naming.DatabaseIdentifier; import org.hibernate.boot.model.naming.Identifier; import org.hibernate.dialect.Dialect; @@ -33,7 +34,7 @@ public InformationExtractorJdbcDatabaseMetaDataImpl(ExtractionContext extraction super( extractionContext ); } - private DatabaseMetaData getJdbcDatabaseMetaData() { + protected DatabaseMetaData getJdbcDatabaseMetaData() { return getExtractionContext().getJdbcDatabaseMetaData(); } @@ -107,11 +108,25 @@ protected T processPrimaryKeysResultSet( } } + @Override + protected T processPrimaryKeysResultSet( + String catalogFilter, + String schemaFilter, + @Nullable String tableName, + ExtractionContext.ResultSetProcessor processor) + throws SQLException { + try ( var resultSet = + getJdbcDatabaseMetaData() + .getPrimaryKeys( catalogFilter, schemaFilter, tableName ) ) { + return processor.process( resultSet ); + } + } + @Override protected T processIndexInfoResultSet( String catalog, String schema, - String table, + @Nullable String table, boolean unique, boolean approximate, ExtractionContext.ResultSetProcessor processor) @@ -127,7 +142,7 @@ protected T processIndexInfoResultSet( protected T processImportedKeysResultSet( String catalog, String schema, - String table, + @Nullable String table, ExtractionContext.ResultSetProcessor processor) throws SQLException { try ( var resultSet = diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorMySQLImpl.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorMySQLImpl.java new file mode 100644 index 000000000000..397e84f39bcc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorMySQLImpl.java @@ -0,0 +1,165 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.tool.schema.extract.internal; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.relational.Namespace; +import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.NameSpaceForeignKeysInformation; +import org.hibernate.tool.schema.extract.spi.NameSpaceIndexesInformation; +import org.hibernate.tool.schema.extract.spi.NameSpacePrimaryKeysInformation; + +import java.sql.SQLException; + +/** + * @since 7.2 + */ +public class InformationExtractorMySQLImpl extends InformationExtractorJdbcDatabaseMetaDataImpl { + + public InformationExtractorMySQLImpl(ExtractionContext extractionContext) { + super( extractionContext ); + } + + @Override + public NameSpaceForeignKeysInformation getForeignKeys(Identifier catalog, Identifier schema) { + final String tableSchema = determineTableSchema( catalog, schema ); + try ( var preparedStatement = getExtractionContext().getJdbcConnection().prepareStatement( getForeignKeysSql( tableSchema ) )) { + if ( tableSchema != null ) { + preparedStatement.setString( 1, tableSchema ); + } + try ( var resultSet = preparedStatement.executeQuery() ) { + return extractNameSpaceForeignKeysInformation( resultSet ); + } + } + catch (SQLException e) { + throw convertSQLException( e, + "Error while reading foreign key information for namespace " + + new Namespace.Name( catalog, schema ) ); + } + } + + private String getForeignKeysSql(String tableSchema) { + final String getForeignKeysSql = """ + select distinct\ + a.referenced_table_schema as PKTABLE_CAT,\ + null as PKTABLE_SCHEM,\ + a.referenced_table_name as PKTABLE_NAME,\ + a.referenced_column_name as PKCOLUMN_NAME,\ + a.table_schema as FKTABLE_CAT,\ + null as FKTABLE_SCHEM,\ + a.table_name AS FKTABLE_NAME,\ + a.column_name as FKCOLUMN_NAME,\ + a.position_in_unique_constraint as KEY_SEQ,\ + case b.update_rule when 'RESTRICT' then 1 when 'NO ACTION' then 3 when 'CASCADE' then 0 when 'SET NULL' then 2 when 'SET DEFAULT' then 4 end as UPDATE_RULE,\ + case b.delete_rule when 'RESTRICT' then 1 when 'NO ACTION' then 3 when 'CASCADE' then 0 when 'SET NULL' then 2 when 'SET DEFAULT' then 4 end as DELETE_RULE,\ + a.constraint_name as FK_NAME,\ + b.unique_constraint_name as PK_NAME + from information_schema.key_column_usage a + join information_schema.referential_constraints b using (constraint_catalog, constraint_schema, constraint_name) + """; + return getForeignKeysSql + (tableSchema == null ? "" : " where a.table_schema = ?") + + " order by a.referenced_table_schema, a.referenced_table_name, a.constraint_name, a.position_in_unique_constraint"; + } + + @Override + public NameSpaceIndexesInformation getIndexes(Identifier catalog, Identifier schema) { + final String tableSchema = determineTableSchema( catalog, schema ); + try ( var preparedStatement = getExtractionContext().getJdbcConnection().prepareStatement( getIndexesSql( tableSchema ) )) { + if ( tableSchema != null ) { + preparedStatement.setString( 1, tableSchema ); + } + try ( var resultSet = preparedStatement.executeQuery() ) { + return extractNameSpaceIndexesInformation( resultSet ); + } + } + catch (SQLException e) { + throw convertSQLException( e, + "Error while reading index information for namespace " + + new Namespace.Name( catalog, schema ) ); + } + } + + private String getIndexesSql(String tableSchema) { + final String getIndexesSql = """ + select distinct\ + a.table_schema as TABLE_CAT,\ + null as TABLE_SCHEM,\ + a.table_name as TABLE_NAME,\ + a.non_unique as NON_UNIQUE,\ + null as INDEX_QUALIFIER,\ + a.index_name as INDEX_NAME,\ + 3 as TYPE,\ + a.seq_in_index as ORDINAL_POSITION,\ + a.column_name as COLUMN_NAME,\ + a.collation as ASC_OR_DESC,\ + a.cardinality as CARDINALITY,\ + 0 as PAGES,\ + null as FILTER_CONDITION + from information_schema.statistics a + """; + return getIndexesSql + (tableSchema == null ? "" : " where a.table_schema = ?") + + " order by a.non_unique, a.index_name, a.seq_in_index"; + } + + @Override + public NameSpacePrimaryKeysInformation getPrimaryKeys(Identifier catalog, Identifier schema) { + final String tableSchema = determineTableSchema( catalog, schema ); + try ( var preparedStatement = getExtractionContext().getJdbcConnection().prepareStatement( getPrimaryKeysSql( tableSchema ) )) { + if ( tableSchema != null ) { + preparedStatement.setString( 1, tableSchema ); + } + try ( var resultSet = preparedStatement.executeQuery() ) { + return extractNameSpacePrimaryKeysInformation( resultSet ); + } + } + catch (SQLException e) { + throw convertSQLException( e, + "Error while reading primary key information for namespace " + + new Namespace.Name( catalog, schema ) ); + } + } + + private String getPrimaryKeysSql(String tableSchema) { + final String getPrimaryKeysSql = """ + select distinct\ + a.table_schema as TABLE_CAT,\ + null as TABLE_SCHEM,\ + a.table_name as TABLE_NAME,\ + a.column_name as COLUMN_NAME,\ + a.seq_in_index as KEY_SEQ,\ + 'PRIMARY' as PK_NAME + from information_schema.statistics a + where a.index_name = 'PRIMARY' + """; + return getPrimaryKeysSql + (tableSchema == null ? "" : " and a.table_schema = ?") + + " order by a.table_schema, a.table_name, a.column_name, a.seq_in_index"; + } + + protected @Nullable String determineTableSchema(@Nullable Identifier catalog, @Nullable Identifier schema) { + if ( catalog != null ) { + return catalog.getText(); + } + if ( schema != null ) { + return schema.getText(); + } + return null; + } + + @Override + public boolean supportsBulkPrimaryKeyRetrieval() { + return true; + } + + @Override + public boolean supportsBulkForeignKeyRetrieval() { + return true; + } + + @Override + public boolean supportsBulkIndexRetrieval() { + return true; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorOracleImpl.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorOracleImpl.java new file mode 100644 index 000000000000..e0f0ffe98ff3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorOracleImpl.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.tool.schema.extract.internal; + +import org.hibernate.tool.schema.extract.spi.ExtractionContext; + +/** + * @since 7.2 + */ +public class InformationExtractorOracleImpl extends InformationExtractorJdbcDatabaseMetaDataImpl { + + public InformationExtractorOracleImpl(ExtractionContext extractionContext) { + super( extractionContext ); + } + + @Override + public boolean supportsBulkPrimaryKeyRetrieval() { + return true; + } + + @Override + public boolean supportsBulkForeignKeyRetrieval() { + return true; + } + + // Unfortunately, there is no support for table wildcard for indexes +} diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorPostgreSQLImpl.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorPostgreSQLImpl.java new file mode 100644 index 000000000000..23ac29fe478c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorPostgreSQLImpl.java @@ -0,0 +1,95 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.tool.schema.extract.internal; + +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.relational.Namespace; +import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.NameSpaceIndexesInformation; + +import java.sql.SQLException; + +/** + * @since 7.2 + */ +public class InformationExtractorPostgreSQLImpl extends InformationExtractorJdbcDatabaseMetaDataImpl { + + public InformationExtractorPostgreSQLImpl(ExtractionContext extractionContext) { + super( extractionContext ); + } + + @Override + public boolean supportsBulkPrimaryKeyRetrieval() { + return true; + } + + @Override + public boolean supportsBulkForeignKeyRetrieval() { + return true; + } + + @Override + public NameSpaceIndexesInformation getIndexes(Identifier catalog, Identifier schema) { + final String tableSchema = schema == null ? null : schema.getText(); + try ( var preparedStatement = getExtractionContext().getJdbcConnection().prepareStatement( getIndexesSql( tableSchema ) )) { + if ( tableSchema != null ) { + preparedStatement.setString( 1, tableSchema ); + } + try ( var resultSet = preparedStatement.executeQuery() ) { + return extractNameSpaceIndexesInformation( resultSet ); + } + } + catch (SQLException e) { + throw convertSQLException( e, + "Error while reading index information for namespace " + + new Namespace.Name( catalog, schema ) ); + } + } + + private String getIndexesSql(String tableSchema) { + final String sql = """ + select\ + current_database() as "TABLE_CAT",\ + n.nspname as "TABLE_SCHEM",\ + ct.relname as "TABLE_NAME",\ + not i.indisunique as "NON_UNIQUE",\ + null as "INDEX_QUALIFIER",\ + ci.relname as "INDEX_NAME",\ + case i.indisclustered\ + when true then 1\ + else\ + case am.amname\ + when 'hash' then 2\ + else 3\ + end\ + end as "TYPE",\ + ic.n as "ORDINAL_POSITION",\ + ci.reltuples as "CARDINALITY",\ + ci.relpages as "PAGES",\ + pg_catalog.pg_get_expr(i.indpred, i.indrelid) as "FILTER_CONDITION",\ + trim(both '"' from pg_catalog.pg_get_indexdef(ci.oid, ic.n, false)) as "COLUMN_NAME",\ + case am.amname\ + when 'btree' then\ + case i.indoption[ic.n - 1] & 1::smallint\ + when 1 then 'D'\ + else 'A'\ + end\ + end as "ASC_OR_DESC" + from pg_catalog.pg_class ct + join pg_catalog.pg_namespace n on (ct.relnamespace = n.oid) + join pg_catalog.pg_index i on (ct.oid = i.indrelid) + join pg_catalog.pg_class ci on (ci.oid = i.indexrelid) + join pg_catalog.pg_am am on (ci.relam = am.oid) + join information_schema._pg_expandarray(i.indkey) ic on 1=1 + """; + return sql + (tableSchema == null ? "" : " where n.nspname = ?"); + } + + @Override + public boolean supportsBulkIndexRetrieval() { + return true; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/ExtractionContext.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/ExtractionContext.java index 5e79c2cc7372..c53d4b160e74 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/ExtractionContext.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/ExtractionContext.java @@ -9,6 +9,7 @@ import java.sql.ResultSet; import java.sql.SQLException; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.Incubating; import org.hibernate.boot.model.naming.Identifier; import org.hibernate.boot.model.relational.QualifiedSequenceName; @@ -63,8 +64,12 @@ interface ResultSetProcessor { */ @Incubating interface DatabaseObjectAccess { - TableInformation locateTableInformation(QualifiedTableName tableName); + @Nullable TableInformation locateTableInformation(QualifiedTableName tableName); SequenceInformation locateSequenceInformation(QualifiedSequenceName sequenceName); + @Nullable PrimaryKeyInformation locatePrimaryKeyInformation(QualifiedTableName tableName); + Iterable locateForeignKeyInformation(QualifiedTableName tableName); + Iterable locateIndexesInformation(QualifiedTableName tableName); + boolean isCaching(); } DatabaseObjectAccess getDatabaseObjectAccess(); diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/InformationExtractor.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/InformationExtractor.java index 8caf3488c781..fe83f0577209 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/InformationExtractor.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/InformationExtractor.java @@ -4,9 +4,9 @@ */ package org.hibernate.tool.schema.extract.spi; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.Incubating; import org.hibernate.boot.model.naming.Identifier; -import org.hibernate.tool.schema.extract.internal.TableInformationImpl; /** * Contract for extracting information about objects in the database schema(s). To an extent, the contract largely @@ -72,7 +72,21 @@ public interface InformationExtractor { * * @return The extracted primary key information */ - PrimaryKeyInformation getPrimaryKey(TableInformationImpl tableInformation); + @Nullable PrimaryKeyInformation getPrimaryKey(TableInformation tableInformation); + + /** + * Extract all the primary keys information. + * + * @param catalog Can be {@code null}, indicating that any catalog may be considered a match. A + * non-{@code null} value indicates that search should be limited to the passed catalog. + * @param schema Can be {@code null}, indicating that any schema may be considered a match. A + * non-{@code null} value indicates that search should be limited to the passed schema . + * + * @return a {@link NameSpacePrimaryKeysInformation} + * @throws SchemaExtractionException when bulk extraction isn't supported + * @since 7.2 + */ + NameSpacePrimaryKeysInformation getPrimaryKeys(Identifier catalog, Identifier schema); /** * Extract information about indexes defined against the given table. Typically called from the TableInformation @@ -84,6 +98,20 @@ public interface InformationExtractor { */ Iterable getIndexes(TableInformation tableInformation); + /** + * Extract all the indexes information. + * + * @param catalog Can be {@code null}, indicating that any catalog may be considered a match. A + * non-{@code null} value indicates that search should be limited to the passed catalog. + * @param schema Can be {@code null}, indicating that any schema may be considered a match. A + * non-{@code null} value indicates that search should be limited to the passed schema . + * + * @return a {@link NameSpaceIndexesInformation} + * @throws SchemaExtractionException when bulk extraction isn't supported + * @since 7.2 + */ + NameSpaceIndexesInformation getIndexes(Identifier catalog, Identifier schema); + /** * Extract information about foreign keys defined on the given table (targeting or point-at other tables). * Typically called from the TableInformation itself as part of on-demand initialization of its state. @@ -93,4 +121,39 @@ public interface InformationExtractor { * @return The extracted foreign-key information */ Iterable getForeignKeys(TableInformation tableInformation); + + /** + * Extract all the foreign keys information. + * + * @param catalog Can be {@code null}, indicating that any catalog may be considered a match. A + * non-{@code null} value indicates that search should be limited to the passed catalog. + * @param schema Can be {@code null}, indicating that any schema may be considered a match. A + * non-{@code null} value indicates that search should be limited to the passed schema . + * + * @return a {@link NameSpaceForeignKeysInformation} + * @throws SchemaExtractionException when bulk extraction isn't supported + * @since 7.2 + */ + NameSpaceForeignKeysInformation getForeignKeys(Identifier catalog, Identifier schema); + + /** + * Can {@link #getPrimaryKeys(Identifier, Identifier)} be used? + * + * @since 7.2 + */ + boolean supportsBulkPrimaryKeyRetrieval(); + + /** + * Can {@link #getForeignKeys(Identifier, Identifier)} be used? + * + * @since 7.2 + */ + boolean supportsBulkForeignKeyRetrieval(); + + /** + * Can {@link #getIndexes(Identifier, Identifier)} be used? + * + * @since 7.2 + */ + boolean supportsBulkIndexRetrieval(); } diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpaceForeignKeysInformation.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpaceForeignKeysInformation.java new file mode 100644 index 000000000000..64e5e08a9135 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpaceForeignKeysInformation.java @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.tool.schema.extract.spi; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; +import org.hibernate.mapping.Table; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @since 7.2 + */ +public class NameSpaceForeignKeysInformation { + private final IdentifierHelper identifierHelper; + private final Map> foreignKeys = new HashMap<>(); + + public NameSpaceForeignKeysInformation(IdentifierHelper identifierHelper) { + this.identifierHelper = identifierHelper; + } + + public void addForeignKeyInformation(TableInformation tableInformation, ForeignKeyInformation foreignKeyInformation) { + foreignKeys.computeIfAbsent( tableInformation.getName().getTableName().getText(), k -> new ArrayList<>() ) + .add( foreignKeyInformation ); + } + + public @Nullable List getForeignKeysInformation(Table table) { + return foreignKeys.get( identifierHelper.toMetaDataObjectName( table.getQualifiedTableName().getTableName() ) ); + } + + public @Nullable List getForeignKeysInformation(String tableName) { + return foreignKeys.get( tableName ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpaceIndexesInformation.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpaceIndexesInformation.java new file mode 100644 index 000000000000..bcb3aa545b7c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpaceIndexesInformation.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.tool.schema.extract.spi; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; +import org.hibernate.mapping.Table; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class NameSpaceIndexesInformation { + private final IdentifierHelper identifierHelper; + private final Map> indexes = new HashMap<>(); + + public NameSpaceIndexesInformation(IdentifierHelper identifierHelper) { + this.identifierHelper = identifierHelper; + } + + public void addIndexInformation(TableInformation tableInformation, IndexInformation indexInformation) { + indexes.computeIfAbsent( tableInformation.getName().getTableName().getText(), k -> new ArrayList<>() ) + .add( indexInformation ); + } + + public @Nullable List getIndexesInformation(Table table) { + return indexes.get( identifierHelper.toMetaDataObjectName( table.getQualifiedTableName().getTableName() ) ); + } + + public @Nullable List getIndexesInformation(String tableName) { + return indexes.get( tableName ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpacePrimaryKeysInformation.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpacePrimaryKeysInformation.java new file mode 100644 index 000000000000..256307c3ded1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpacePrimaryKeysInformation.java @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.tool.schema.extract.spi; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; +import org.hibernate.mapping.Table; + +import java.util.HashMap; +import java.util.Map; + +/** + * @since 7.2 + */ +public class NameSpacePrimaryKeysInformation { + private final IdentifierHelper identifierHelper; + private final Map primaryKeys = new HashMap<>(); + + public NameSpacePrimaryKeysInformation(IdentifierHelper identifierHelper) { + this.identifierHelper = identifierHelper; + } + + public void addPrimaryKeyInformation(TableInformation tableInformation, PrimaryKeyInformation primaryKeyInformation) { + primaryKeys.put( tableInformation.getName().getTableName().getText(), primaryKeyInformation ); + } + + public @Nullable PrimaryKeyInformation getPrimaryKeyInformation(Table table) { + return primaryKeys.get( identifierHelper.toMetaDataObjectName( table.getQualifiedTableName().getTableName() ) ); + } + + public @Nullable PrimaryKeyInformation getPrimaryKeyInformation(String tableName) { + return primaryKeys.get( tableName ); + } + + public void validate() { + for ( Map.Entry entry : primaryKeys.entrySet() ) { + final var tableName = entry.getKey(); + final var primaryKeyInformation = entry.getValue(); + int i = 1; + for ( ColumnInformation column : primaryKeyInformation.getColumns() ) { + if ( column == null ) { + throw new SchemaExtractionException( + "Primary Key information was missing for key [" + + primaryKeyInformation.getPrimaryKeyIdentifier() + "] on table [" + tableName + + "] at KEY_SEQ = " + i + ); + } + i++; + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpaceTablesInformation.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpaceTablesInformation.java index f7342ef4a2a7..9b218579f29e 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpaceTablesInformation.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpaceTablesInformation.java @@ -7,6 +7,7 @@ import java.util.HashMap; import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; import org.hibernate.mapping.Table; @@ -25,11 +26,11 @@ public void addTableInformation(TableInformation tableInformation) { tables.put( tableInformation.getName().getTableName().getText(), tableInformation ); } - public TableInformation getTableInformation(Table table) { + public @Nullable TableInformation getTableInformation(Table table) { return tables.get( identifierHelper.toMetaDataObjectName( table.getQualifiedTableName().getTableName() ) ); } - public TableInformation getTableInformation(String tableName) { + public @Nullable TableInformation getTableInformation(String tableName) { return tables.get( tableName ); } } 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 971466b11d8f..1279029091aa 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 @@ -25,6 +25,7 @@ import org.hibernate.mapping.ForeignKey; import org.hibernate.mapping.Index; import org.hibernate.mapping.Table; +import org.hibernate.resource.transaction.spi.DdlTransactionIsolator; import org.hibernate.tool.schema.UniqueConstraintSchemaUpdateStrategy; import org.hibernate.tool.schema.extract.spi.DatabaseInformation; import org.hibernate.tool.schema.extract.spi.IndexInformation; @@ -80,8 +81,7 @@ public void doMigration( if ( !targetDescriptor.getTargetTypes().isEmpty() ) { final var jdbcContext = tool.resolveJdbcContext( options.getConfigurationValues() ); try ( var isolator = tool.getDdlTransactionIsolator( jdbcContext ) ) { - final var databaseInformation = - buildDatabaseInformation( isolator, sqlGenerationContext, tool ); + final var databaseInformation = buildDatabaseInformation( isolator, sqlGenerationContext ); final var targets = tool.buildGenerationTargets( targetDescriptor, isolator, @@ -127,6 +127,12 @@ public void doMigration( } } + protected DatabaseInformation buildDatabaseInformation( + DdlTransactionIsolator ddlTransactionIsolator, + SqlStringGenerationContext sqlStringGenerationContext) { + return Helper.buildDatabaseInformation( ddlTransactionIsolator, sqlStringGenerationContext, tool ); + } + private SqlStringGenerationContext sqlGenerationContext(Metadata metadata, ExecutionOptions options) { return SqlStringGenerationContextImpl.fromConfigurationMapForMigration( tool.getServiceRegistry().requireService( JdbcEnvironment.class ), diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/GroupedSchemaMigratorImpl.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/GroupedSchemaMigratorImpl.java index fe3c884cc3d8..31ddc8d1a32a 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/GroupedSchemaMigratorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/GroupedSchemaMigratorImpl.java @@ -4,6 +4,7 @@ */ package org.hibernate.tool.schema.internal; +import java.sql.SQLException; import java.util.Set; import org.hibernate.boot.Metadata; @@ -11,7 +12,10 @@ import org.hibernate.boot.model.relational.Namespace; import org.hibernate.boot.model.relational.SqlStringGenerationContext; import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.engine.jdbc.internal.Formatter; +import org.hibernate.resource.transaction.spi.DdlTransactionIsolator; +import org.hibernate.tool.schema.extract.internal.CachingDatabaseInformationImpl; import org.hibernate.tool.schema.extract.spi.DatabaseInformation; import org.hibernate.tool.schema.extract.spi.NameSpaceTablesInformation; import org.hibernate.tool.schema.spi.GenerationTarget; @@ -99,4 +103,23 @@ else if ( tableInformation.isPhysicalTable() ) { } return tablesInformation; } + + @Override + protected DatabaseInformation buildDatabaseInformation(DdlTransactionIsolator ddlTransactionIsolator, SqlStringGenerationContext sqlStringGenerationContext) { + final var serviceRegistry = ddlTransactionIsolator.getJdbcContext().getServiceRegistry(); + final var jdbcEnvironment = serviceRegistry.requireService( JdbcEnvironment.class ); + try { + return new CachingDatabaseInformationImpl( + serviceRegistry, + jdbcEnvironment, + sqlStringGenerationContext, + ddlTransactionIsolator, + tool + ); + } + catch (SQLException e) { + throw jdbcEnvironment.getSqlExceptionHelper() + .convert( e, "Unable to build DatabaseInformation" ); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/HibernateSchemaManagementTool.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/HibernateSchemaManagementTool.java index 3ac601d90353..f5773e1ba974 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/HibernateSchemaManagementTool.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/HibernateSchemaManagementTool.java @@ -21,7 +21,6 @@ import org.hibernate.service.spi.ServiceRegistryImplementor; import org.hibernate.tool.schema.JdbcMetadataAccessStrategy; import org.hibernate.tool.schema.TargetType; -import org.hibernate.tool.schema.extract.internal.InformationExtractorJdbcDatabaseMetaDataImpl; import org.hibernate.tool.schema.extract.spi.ExtractionContext; import org.hibernate.tool.schema.extract.spi.InformationExtractor; import org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase; @@ -469,7 +468,7 @@ public ExtractionContext createExtractionContext( @Override public InformationExtractor createInformationExtractor(ExtractionContext extractionContext) { - return new InformationExtractorJdbcDatabaseMetaDataImpl( extractionContext ); + return extractionContext.getJdbcEnvironment().getDialect().getInformationExtractor( extractionContext ); } } }