From 5e3e4f17241c22c56eac6a6fe164f3782f262103 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Tue, 30 Sep 2025 19:24:41 +0200 Subject: [PATCH] HHH-13843 Create InformationExtractor through Dialect to allow batch loading Cache the result of the batch loaded tables, foreign keys, primary keys and indexes to serve objects looked up by qualified name from cache. This reduces the amount of server round trips on schema migration. Ultimately, this per dialect flexibility is necessary to implement UDT validation and migration --- .../dialect/CockroachLegacyDialect.java | 7 + .../community/dialect/GaussDBDialect.java | 8 + .../dialect/OracleLegacyDialect.java | 7 + .../dialect/PostgreSQLLegacyDialect.java | 8 + .../hibernate/dialect/CockroachDialect.java | 8 + .../java/org/hibernate/dialect/Dialect.java | 13 + .../org/hibernate/dialect/MySQLDialect.java | 7 + .../org/hibernate/dialect/OracleDialect.java | 8 + .../hibernate/dialect/PostgreSQLDialect.java | 7 + .../AbstractInformationExtractorImpl.java | 289 +++++++++++++++++- .../CachingDatabaseInformationImpl.java | 109 +++++++ .../internal/DatabaseInformationImpl.java | 43 ++- ...tionExtractorJdbcDatabaseMetaDataImpl.java | 21 +- .../InformationExtractorMySQLImpl.java | 165 ++++++++++ .../InformationExtractorOracleImpl.java | 29 ++ .../InformationExtractorPostgreSQLImpl.java | 95 ++++++ .../schema/extract/spi/ExtractionContext.java | 7 +- .../extract/spi/InformationExtractor.java | 67 +++- .../spi/NameSpaceForeignKeysInformation.java | 39 +++ .../spi/NameSpaceIndexesInformation.java | 36 +++ .../spi/NameSpacePrimaryKeysInformation.java | 54 ++++ .../spi/NameSpaceTablesInformation.java | 5 +- .../internal/AbstractSchemaMigrator.java | 10 +- .../internal/GroupedSchemaMigratorImpl.java | 23 ++ .../HibernateSchemaManagementTool.java | 3 +- 25 files changed, 1048 insertions(+), 20 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/CachingDatabaseInformationImpl.java create mode 100644 hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorMySQLImpl.java create mode 100644 hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorOracleImpl.java create mode 100644 hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/InformationExtractorPostgreSQLImpl.java create mode 100644 hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpaceForeignKeysInformation.java create mode 100644 hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpaceIndexesInformation.java create mode 100644 hibernate-core/src/main/java/org/hibernate/tool/schema/extract/spi/NameSpacePrimaryKeysInformation.java 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 ); } } }