From 7007927abe73eaaa1d54f405331d0a695666bb92 Mon Sep 17 00:00:00 2001 From: Davide D'Alto Date: Fri, 21 Mar 2025 13:07:02 +0100 Subject: [PATCH 1/5] [#2129] Add DatabaseConfiguration#getMinimumVersion We use it to test the dialect selection at start up --- .../containers/DatabaseConfiguration.java | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DatabaseConfiguration.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DatabaseConfiguration.java index f0d497ee2..c0a081126 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DatabaseConfiguration.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DatabaseConfiguration.java @@ -5,6 +5,7 @@ */ package org.hibernate.reactive.containers; +import java.lang.reflect.Field; import java.util.Arrays; import java.util.Map; import java.util.Objects; @@ -12,6 +13,7 @@ import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; +import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.MariaDBDialect; import org.hibernate.dialect.MySQLDialect; @@ -28,15 +30,16 @@ public class DatabaseConfiguration { public static final boolean USE_DOCKER = Boolean.getBoolean("docker"); public enum DBType { - DB2( DB2Database.INSTANCE, 50000, "com.ibm.db2.jcc.DB2Driver", DB2Dialect.class ), - MYSQL( MySQLDatabase.INSTANCE, 3306, "com.mysql.cj.jdbc.Driver", MySQLDialect.class ), - MARIA( MariaDatabase.INSTANCE, 3306, "org.mariadb.jdbc.Driver", MariaDBDialect.class, "mariadb" ), - POSTGRESQL( PostgreSQLDatabase.INSTANCE, 5432, "org.postgresql.Driver", PostgreSQLDialect.class, "POSTGRES", "PG" ), - COCKROACHDB( CockroachDBDatabase.INSTANCE, 26257, "org.postgresql.Driver", CockroachDialect.class, "COCKROACH" ), - SQLSERVER( MSSQLServerDatabase.INSTANCE, 1433, "com.microsoft.sqlserver.jdbc.SQLServerDriver", SQLServerDialect.class, "MSSQL", "MSSQLSERVER" ), - ORACLE( OracleDatabase.INSTANCE, 1521, "oracle.jdbc.OracleDriver", OracleDialect.class ); + DB2( DB2Database.INSTANCE, "DB2", 50000, "com.ibm.db2.jcc.DB2Driver", DB2Dialect.class ), + MYSQL( MySQLDatabase.INSTANCE, "MySQL",3306, "com.mysql.cj.jdbc.Driver", MySQLDialect.class ), + MARIA( MariaDatabase.INSTANCE, "MariaDB",3306, "org.mariadb.jdbc.Driver", MariaDBDialect.class, "mariadb" ), + POSTGRESQL( PostgreSQLDatabase.INSTANCE, "PostgreSQL", 5432, "org.postgresql.Driver", PostgreSQLDialect.class, "POSTGRES", "PG" ), + COCKROACHDB( CockroachDBDatabase.INSTANCE, "CockroachDb", 26257, "org.postgresql.Driver", CockroachDialect.class, "COCKROACH" ), + SQLSERVER( MSSQLServerDatabase.INSTANCE, "Microsoft SQL Server", 1433, "com.microsoft.sqlserver.jdbc.SQLServerDriver", SQLServerDialect.class, "MSSQL", "MSSQLSERVER" ), + ORACLE( OracleDatabase.INSTANCE, "Oracle", 1521, "oracle.jdbc.OracleDriver", OracleDialect.class ); private final TestableDatabase configuration; + private final String productName; private final int defaultPort; // A list of alternative names that can be used to select the db @@ -47,8 +50,9 @@ public enum DBType { private final Class dialect; - DBType(TestableDatabase configuration, int defaultPort, String jdbcDriver, Class dialect, String... aliases) { + DBType(TestableDatabase configuration, String productName, int defaultPort, String jdbcDriver, Class dialect, String... aliases) { this.configuration = configuration; + this.productName = productName; this.defaultPort = defaultPort; this.aliases = aliases; this.dialect = dialect; @@ -81,6 +85,10 @@ public static DBType fromString(String dbName) { .toString( DBType.values() ) ); } + public String getProductName() { + return productName; + } + public int getDefaultPort() { return defaultPort; } @@ -92,6 +100,24 @@ public String getJdbcDriver() { public Class getDialectClass() { return dialect; } + + /** + * The minimum version of the database supported by the dialect. + *

+ * We use reflection because it's not accessible from the tests. + * Copied from MetadataAccessTests in Hibernate ORM. + *

+ */ + public DatabaseVersion getMinimumVersion() { + try { + Field field = dialect.getDeclaredField( "MINIMUM_VERSION" ); + field.setAccessible( true ); + return (DatabaseVersion) field.get( null ); + } + catch (IllegalAccessException | NoSuchFieldException e) { + throw new RuntimeException( "Error extracting 'MINIMUM_VERSION' from '" + dialect + "'", e ); + } + } } public static final String USERNAME = "hreact"; @@ -101,7 +127,7 @@ public Class getDialectClass() { private static DBType dbType; public static DBType dbType() { - if (dbType == null) { + if ( dbType == null ) { String dbTypeString = System.getProperty( "db", DBType.POSTGRESQL.name() ); dbType = DBType.fromString( dbTypeString ); System.out.println( "Using database type: " + dbType.name() ); @@ -131,5 +157,4 @@ public static String expectedDatatype(Class dataType) { private DatabaseConfiguration() { } - } From ced1a6dd308ccdbd2484f6fd88e75a74553b7597 Mon Sep 17 00:00:00 2001 From: Davide D'Alto Date: Fri, 21 Mar 2025 13:10:00 +0100 Subject: [PATCH 2/5] [#2129] Set the logging properties in a separate method This way we can reuse the method even if the tests don't extend BaseReactiveTest --- .../hibernate/reactive/BaseReactiveTest.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BaseReactiveTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BaseReactiveTest.java index 1a5f51e3b..07a79379b 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BaseReactiveTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BaseReactiveTest.java @@ -92,6 +92,18 @@ protected void setProperties(Configuration configuration) { setDefaultProperties( configuration ); } + /** + * Set the properties related to the SQL logs. + * Some tests won't extend this class, but we still want to apply the same rules for these kinds of properties. + */ + public static void setSqlLoggingProperties(Configuration configuration) { + // Use JAVA_TOOL_OPTIONS='-Dhibernate.show_sql=true' + // Keep the default to false, otherwise the log on CI becomes too big + configuration.setProperty( Settings.SHOW_SQL, System.getProperty( Settings.SHOW_SQL, "false" ) ); + configuration.setProperty( Settings.FORMAT_SQL, System.getProperty( Settings.FORMAT_SQL, "false" ) ); + configuration.setProperty( Settings.HIGHLIGHT_SQL, System.getProperty( Settings.HIGHLIGHT_SQL, "true" ) ); + } + /** * Configure default properties common to most tests. */ @@ -102,13 +114,9 @@ public static void setDefaultProperties(Configuration configuration) { configuration.setProperty( Settings.HBM2DDL_IMPORT_FILES, "/db2.sql" ); doneTablespace = true; } - // Use JAVA_TOOL_OPTIONS='-Dhibernate.show_sql=true' - // Keep the default to false, otherwise the log on CI becomes too big - configuration.setProperty( Settings.SHOW_SQL, System.getProperty( Settings.SHOW_SQL, "false" ) ); - configuration.setProperty( Settings.FORMAT_SQL, System.getProperty( Settings.FORMAT_SQL, "false" ) ); - configuration.setProperty( Settings.HIGHLIGHT_SQL, System.getProperty( Settings.HIGHLIGHT_SQL, "true" ) ); configuration.setProperty( PersistentTableStrategy.DROP_ID_TABLES, "true" ); configuration.setProperty( GlobalTemporaryTableStrategy.DROP_ID_TABLES, "true" ); + setSqlLoggingProperties( configuration ); } public static final SessionFactoryManager factoryManager = new SessionFactoryManager(); From 9d09fef640eb8ee84746f272ad72df409c572ffc Mon Sep 17 00:00:00 2001 From: Davide D'Alto Date: Fri, 21 Mar 2025 13:27:57 +0100 Subject: [PATCH 3/5] [#2129] Remove StandAloneReactiveTest I'm not sure what's the purpose of this test, and why it was working before. But, it doesn't work anymore with the fix for #2129 because some properties are missing and we are not setting the right credentials. I think it was one of first tests we introduced when we have created the project and obsolete now. --- .../reactive/StandAloneReactiveTest.java | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/StandAloneReactiveTest.java diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/StandAloneReactiveTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/StandAloneReactiveTest.java deleted file mode 100644 index 29ef9de85..000000000 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/StandAloneReactiveTest.java +++ /dev/null @@ -1,36 +0,0 @@ -/* Hibernate, Relational Persistence for Idiomatic Java - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright: Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.reactive; - -import org.hibernate.boot.MetadataSources; -import org.hibernate.boot.registry.StandardServiceRegistry; -import org.hibernate.dialect.PostgreSQLDialect; -import org.hibernate.reactive.provider.Settings; -import org.hibernate.reactive.provider.ReactiveServiceRegistryBuilder; -import org.hibernate.reactive.stage.Stage; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class StandAloneReactiveTest { - - @Test - public void createReactiveSessionFactory() { - StandardServiceRegistry registry = new ReactiveServiceRegistryBuilder() - .applySetting( Settings.TRANSACTION_COORDINATOR_STRATEGY, "jta" ) - .applySetting( Settings.DIALECT, PostgreSQLDialect.class.getName() ) - .applySetting( Settings.URL, "jdbc:postgresql://localhost/hreact?user=none" ) - .build(); - - Stage.SessionFactory factory = new MetadataSources( registry ) - .buildMetadata() - .getSessionFactoryBuilder() - .build() - .unwrap( Stage.SessionFactory.class ); - - assertThat( factory ).isNotNull(); - } -} From 6ce3578c311da1155b5016b3f74400d60015c0b7 Mon Sep 17 00:00:00 2001 From: Davide D'Alto Date: Fri, 21 Mar 2025 13:11:13 +0100 Subject: [PATCH 4/5] [#2129] Allow offline startup and on-demand DB version checks This feature is useful in particular for applications that start up before the DB becomes accessible. It can also be useful for Quarkus, where (part of) startup happens at build time. This achieved by setting the property: ``` hibernate.boot.allow_jdbc_metadata_access = false ``` --- .../ReactiveStandardDialectResolver.java | 10 +- .../service/NoJdbcEnvironmentInitiator.java | 200 ++++++++++++++---- 2 files changed, 167 insertions(+), 43 deletions(-) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/dialect/internal/ReactiveStandardDialectResolver.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/dialect/internal/ReactiveStandardDialectResolver.java index 1b24e567d..d12791e5f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/dialect/internal/ReactiveStandardDialectResolver.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/dialect/internal/ReactiveStandardDialectResolver.java @@ -11,24 +11,22 @@ import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.jdbc.dialect.spi.DialectResolver; -import static org.hibernate.dialect.CockroachDialect.parseVersion; public class ReactiveStandardDialectResolver implements DialectResolver { @Override public Dialect resolveDialect(DialectResolutionInfo info) { - // Hibernate ORM runs an extra query to recognize CockroachDB from PostgreSQL - // We've already done it, so we are trying to skip that step + // Hibernate ORM runs an extra query to recognize CockroachDB from PostgresSQL + // We already did it when we created the DialectResolutionInfo in NoJdbcEnvironmentInitiator, + // so we can skip that step here. if ( info.getDatabaseName().startsWith( "Cockroach" ) ) { - return new CockroachDialect( parseVersion( info.getDatabaseVersion() ) ); + return new CockroachDialect( info ); } - for ( Database database : Database.values() ) { if ( database.matchesResolutionInfo( info ) ) { return database.createDialect( info ); } } - return null; } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcEnvironmentInitiator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcEnvironmentInitiator.java index 953285dd4..0cf59c413 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcEnvironmentInitiator.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcEnvironmentInitiator.java @@ -5,25 +5,29 @@ */ package org.hibernate.reactive.provider.service; -import java.util.Map; -import java.util.concurrent.CompletionStage; - import org.hibernate.boot.registry.StandardServiceInitiator; import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.connections.spi.DatabaseConnectionInfo; import org.hibernate.engine.jdbc.dialect.spi.DialectFactory; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentImpl; +import org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; import org.hibernate.reactive.pool.ReactiveConnection; import org.hibernate.reactive.pool.ReactiveConnectionPool; -import org.hibernate.reactive.provider.Settings; import org.hibernate.reactive.util.impl.CompletionStages; import org.hibernate.service.ServiceRegistry; import org.hibernate.service.spi.ServiceRegistryImplementor; import io.vertx.sqlclient.spi.DatabaseMetadata; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import static java.lang.Integer.parseInt; +import static java.util.Objects.requireNonNullElse; import static java.util.function.Function.identity; import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; @@ -32,7 +36,8 @@ * that provides an implementation of {@link JdbcEnvironment} that infers * the Hibernate {@link org.hibernate.dialect.Dialect} from the JDBC URL. */ -public class NoJdbcEnvironmentInitiator implements StandardServiceInitiator { +public class NoJdbcEnvironmentInitiator extends JdbcEnvironmentInitiator + implements StandardServiceInitiator { public static final NoJdbcEnvironmentInitiator INSTANCE = new NoJdbcEnvironmentInitiator(); @@ -42,14 +47,59 @@ public Class getServiceInitiated() { } @Override - public JdbcEnvironment initiateService(Map configurationValues, ServiceRegistryImplementor registry) { - boolean explicitDialect = configurationValues.containsKey( Settings.DIALECT ); - if ( explicitDialect ) { - DialectFactory dialectFactory = registry.getService( DialectFactory.class ); - return new JdbcEnvironmentImpl( registry, dialectFactory.buildDialect( configurationValues, null ) ); - } + protected void logConnectionInfo(DatabaseConnectionInfo databaseConnectionInfo) { + // Nothing to do we log the connection info somewhere else + } + + @Override + protected JdbcEnvironmentImpl getJdbcEnvironmentWithExplicitConfiguration( + Map configurationValues, + ServiceRegistryImplementor registry, + DialectFactory dialectFactory, + DialectResolutionInfo dialectResolutionInfo) { + return super.getJdbcEnvironmentWithExplicitConfiguration( + configurationValues, + registry, + dialectFactory, + dialectResolutionInfo + ); + } - return new JdbcEnvironmentImpl( registry, new DialectBuilder( configurationValues, registry ).build() ); + @Override + protected JdbcEnvironmentImpl getJdbcEnvironmentWithDefaults( + Map configurationValues, + ServiceRegistryImplementor registry, + DialectFactory dialectFactory) { + return new JdbcEnvironmentImpl( registry, new DialectBuilder( configurationValues, registry ) + .build( dialectFactory ) + ); + } + + @Override + protected JdbcEnvironmentImpl getJdbcEnvironmentUsingJdbcMetadata( + Map configurationValues, + ServiceRegistryImplementor registry, + DialectFactory dialectFactory, + String explicitDatabaseName, + Integer explicitDatabaseMajorVersion, + Integer explicitDatabaseMinorVersion, + String explicitDatabaseVersion) { + try { + final Dialect dialect = new DialectBuilder( configurationValues, registry ) + .build( + dialectFactory, + new ExplicitMetadata( + explicitDatabaseName, + explicitDatabaseMajorVersion, + explicitDatabaseMinorVersion, + explicitDatabaseVersion + ) + ); + return new JdbcEnvironmentImpl( registry, dialect ); + } + catch (RuntimeException e) { + return getJdbcEnvironmentWithDefaults( configurationValues, registry, dialectFactory ); + } } private static class DialectBuilder { @@ -62,24 +112,40 @@ public DialectBuilder(Map configurationValues, ServiceRegistry r this.registry = registry; } - public Dialect build() { - DialectFactory dialectFactory = registry.getService( DialectFactory.class ); + public Dialect build(DialectFactory dialectFactory) { return dialectFactory.buildDialect( configurationValues, this::dialectResolutionInfo ); } + public Dialect build(DialectFactory dialectFactory, ExplicitMetadata explicitMetadata) { + return dialectFactory.buildDialect( configurationValues, () -> dialectResolutionInfo( explicitMetadata ) ); + } + private DialectResolutionInfo dialectResolutionInfo() { - ReactiveConnectionPool connectionPool = registry.getService( ReactiveConnectionPool.class ); - return connectionPool + return dialectResolutionInfo( DialectBuilder::buildResolutionInfo ); + } + + private DialectResolutionInfo dialectResolutionInfo(ExplicitMetadata explicitMetadata) { + return dialectResolutionInfo( reactiveConnection -> DialectBuilder + .buildResolutionInfo( reactiveConnection, explicitMetadata ) + ); + } + + private DialectResolutionInfo dialectResolutionInfo(Function> dialectResolutionFunction) { + return registry + .getService( ReactiveConnectionPool.class ) // The default SqlExceptionHelper in ORM requires the dialect, but we haven't created a dialect yet, // so we need to override it at this stage, or we will have an exception. .getConnection( new SqlExceptionHelper( true ) ) - .thenCompose( DialectBuilder::buildResolutionInfo ) + .thenCompose( dialectResolutionFunction ) .toCompletableFuture().join(); } private static CompletionStage buildResolutionInfo(ReactiveConnection connection) { - final DatabaseMetadata databaseMetadata = connection.getDatabaseMetadata(); - return resolutionInfoStage( connection, databaseMetadata ) + return buildResolutionInfo( connection, null ); + } + + private static CompletionStage buildResolutionInfo(ReactiveConnection connection, ExplicitMetadata explicitMetadata) { + return resolutionInfoStage( connection, explicitMetadata ) .handle( CompletionStages::handle ) .thenCompose( handled -> { if ( handled.hasFailed() ) { @@ -96,19 +162,27 @@ private static CompletionStage buildResolutionInf } ); } - private static CompletionStage resolutionInfoStage(ReactiveConnection connection, DatabaseMetadata databaseMetadata) { - if ( databaseMetadata.productName().equalsIgnoreCase( "PostgreSQL" ) ) { - // We need to check if the database is PostgreSQL or CockroachDB - // Hibernate ORM does it using a query, so we need to check in advance + /** + * @see org.hibernate.dialect.Database#POSTGRESQL for recognizing CockroachDB + */ + private static CompletionStage resolutionInfoStage(ReactiveConnection connection, ExplicitMetadata explicitMetadata) { + final DatabaseMetadata databaseMetadata = explicitMetadata != null + ? new ReactiveDatabaseMetadata( connection.getDatabaseMetadata(), explicitMetadata ) + : connection.getDatabaseMetadata(); + + // If the product name is explicitly set to Postgres, we are not going to override it + if ( ( explicitMetadata == null || explicitMetadata.productName == null ) + && databaseMetadata.productName().equalsIgnoreCase( "PostgreSQL" ) ) { + // CockroachDB returns "PostgreSQL" as product name in the metadata. + // So, we need to check if the database is PostgreSQL or CockroachDB + // We follow the same approach used by ORM: run a new query and check the full version metadata // See org.hibernate.dialect.Database.POSTGRESQL#createDialect return connection.select( "select version()" ) .thenApply( DialectBuilder::readFullVersion ) - .thenApply( fullversion -> { - if ( fullversion.startsWith( "Cockroach" ) ) { - return new CockroachDatabaseMetadata( fullversion ); - } - return databaseMetadata; - } ) + .thenApply( fullVersion -> fullVersion.startsWith( "Cockroach" ) + ? new ReactiveDatabaseMetadata( "Cockroach", databaseMetadata ) + : databaseMetadata + ) .thenApply( ReactiveDialectResolutionInfo::new ); } @@ -122,32 +196,62 @@ private static String readFullVersion(ReactiveConnection.Result result) { } } - private static class CockroachDatabaseMetadata implements DatabaseMetadata { + /** + * Utility class to pass around explicit metadata properties. + * It's different from {@link DatabaseMetadata} because values can be null. + */ + private static class ExplicitMetadata { + private final String productName; + private final String fullVersion; + private final Integer majorVersion; + private final Integer minorVersion; + + public ExplicitMetadata(String explicitDatabaseName, Integer explicitDatabaseMajorVersion, Integer explicitDatabaseMinorVersion, String explicitDatabaseVersion ) { + this.productName = explicitDatabaseName; + this.fullVersion = explicitDatabaseVersion; + this.majorVersion = explicitDatabaseMajorVersion; + this.minorVersion = explicitDatabaseMinorVersion; + } + } - private final String fullversion; + private static class ReactiveDatabaseMetadata implements DatabaseMetadata { + public final String productName; + public final String fullVersion; + public final int majorVersion; + public final int minorVersion; + + public ReactiveDatabaseMetadata(String productName, DatabaseMetadata databaseMetadata) { + this.productName = productName; + this.fullVersion = databaseMetadata.productName(); + this.majorVersion = databaseMetadata.majorVersion(); + this.minorVersion = databaseMetadata.minorVersion(); + } - public CockroachDatabaseMetadata(String fullversion) { - this.fullversion = fullversion; + public ReactiveDatabaseMetadata(DatabaseMetadata metadata, ExplicitMetadata explicitMetadata) { + productName = requireNonNullElse( explicitMetadata.productName, metadata.productName() ); + fullVersion = requireNonNullElse( explicitMetadata.fullVersion, metadata.fullVersion() ); + majorVersion = requireNonNullElse( explicitMetadata.majorVersion, metadata.majorVersion() ); + minorVersion = requireNonNullElse( explicitMetadata.minorVersion, metadata.minorVersion() ); } @Override public String productName() { - return "CockroachDb"; + return productName; } @Override public String fullVersion() { - return fullversion; + return fullVersion; } @Override public int majorVersion() { - return 0; + return majorVersion; } @Override public int minorVersion() { - return 0; + return minorVersion; } } @@ -179,6 +283,27 @@ public int getDatabaseMinorVersion() { return metadata.minorVersion(); } + @Override + public int getDatabaseMicroVersion() { + return databaseMicroVersion( metadata.fullVersion(), metadata.majorVersion(), metadata.minorVersion() ); + } + + // We should move this in ORM and avoid duplicated code + private static int databaseMicroVersion(String version, int major, int minor) { + final String prefix = major + "." + minor + "."; + if ( version.startsWith( prefix ) ) { + try { + final String substring = version.substring( prefix.length() ); + final String micro = new StringTokenizer( substring, " .,-:;/()[]" ).nextToken(); + return parseInt( micro ); + } + catch (NumberFormatException nfe) { + return 0; + } + } + return 0; + } + @Override public String getDriverName() { return getDatabaseName(); @@ -196,6 +321,7 @@ public int getDriverMinorVersion() { @Override public String getSQLKeywords() { + // Vert.x metadata doesn't have this info return null; } From 18387d6158244d3b3f78cbf70a5e57339080d6df Mon Sep 17 00:00:00 2001 From: Davide D'Alto Date: Fri, 21 Mar 2025 13:16:10 +0100 Subject: [PATCH 5/5] [#2129] Test hibernate.boot.allow_jdbc_metadata_access Hibernate Reactive should be able to start even if there's no database when `hibernate.boot.allow_jdbc_metadata_access = false`. --- .../reactive/MetadataAccessTest.java | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/MetadataAccessTest.java diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MetadataAccessTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MetadataAccessTest.java new file mode 100644 index 000000000..da1578045 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MetadataAccessTest.java @@ -0,0 +1,260 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import org.hibernate.HibernateException; +import org.hibernate.boot.registry.StandardServiceInitiator; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.Configuration; +import org.hibernate.dialect.DatabaseVersion; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl; +import org.hibernate.engine.jdbc.dialect.spi.DialectFactory; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfoSource; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.reactive.containers.DatabaseConfiguration; +import org.hibernate.reactive.provider.ReactiveServiceRegistryBuilder; +import org.hibernate.reactive.provider.Settings; +import org.hibernate.reactive.testing.SqlStatementTracker; +import org.hibernate.service.spi.ServiceRegistryImplementor; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Map; +import java.util.Properties; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.cfg.JdbcSettings.ALLOW_METADATA_ON_BOOT; +import static org.hibernate.cfg.JdbcSettings.DIALECT; +import static org.hibernate.cfg.JdbcSettings.JAKARTA_HBM2DDL_DB_NAME; +import static org.hibernate.cfg.JdbcSettings.JAKARTA_JDBC_URL; +import static org.hibernate.reactive.BaseReactiveTest.setSqlLoggingProperties; +import static org.hibernate.reactive.containers.DatabaseConfiguration.dbType; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * Hibernate ORM allows starting up without access to the DB ("offline") + * when {@link Settings#ALLOW_METADATA_ON_BOOT} is set to false. + *

+ * Inspired by the test + * {@code org.hibernate.orm.test.boot.database.metadata.MetadataAccessTests} + * in Hibernate ORM. + *

+ */ +public class MetadataAccessTest { + + private static SqlStatementTracker sqlTracker; + + private static final int EXPECTED_MAJOR = 123; + private static final int EXPECTED_MINOR = 456; + private static final DatabaseVersion EXPECTED_VERSION = DatabaseVersion.make( EXPECTED_MAJOR, EXPECTED_MINOR ); + + private static Properties dialectMajorMinorProperties() { + Properties dbProperties = new Properties(); + // Major and Minor should override the full version, so we keep them different + dbProperties.setProperty( Settings.DIALECT_DB_MAJOR_VERSION, String.valueOf( EXPECTED_MAJOR ) ); + dbProperties.setProperty( Settings.DIALECT_DB_MINOR_VERSION, String.valueOf( EXPECTED_MINOR ) ); + return dbProperties; + } + + private static Properties jakartaMajorMinorProperties() { + Properties dbProperties = new Properties(); + dbProperties.setProperty( Settings.JAKARTA_HBM2DDL_DB_MAJOR_VERSION, String.valueOf( EXPECTED_MAJOR ) ); + dbProperties.setProperty( Settings.JAKARTA_HBM2DDL_DB_MINOR_VERSION, String.valueOf( EXPECTED_MINOR ) ); + return dbProperties; + } + + private static Properties jakartaFullDbVersion() { + Properties dbProperties = new Properties(); + dbProperties.setProperty( Settings.JAKARTA_HBM2DDL_DB_VERSION, EXPECTED_MAJOR + "." + EXPECTED_MINOR ); + return dbProperties; + } + + private static Properties dialectFullDbVersion() { + Properties dbProperties = new Properties(); + dbProperties.setProperty( Settings.DIALECT_DB_VERSION, EXPECTED_MAJOR + "." + EXPECTED_MINOR ); + return dbProperties; + } + + static Stream explicitVersionProperties() { + return Stream.of( + arguments( "Jakarta properties", jakartaMajorMinorProperties() ), + arguments( "Deprecated dialect properties", dialectMajorMinorProperties() ), + arguments( "Jakarta db version property", jakartaFullDbVersion() ), + arguments( "Deprecated dialect db version property", dialectFullDbVersion() ) + ); + } + + @ParameterizedTest(name = "Test {0} with " + DIALECT) + @MethodSource("explicitVersionProperties") + public void testExplicitVersionWithDialect(String display, Properties dbProperties) { + dbProperties.setProperty( ALLOW_METADATA_ON_BOOT, "false" ); + dbProperties.setProperty( DIALECT, dbType().getDialectClass().getName() ); + + try (StandardServiceRegistry serviceRegistry = createServiceRegistry( dbProperties )) { + final Dialect dialect = dialect( serviceRegistry ); + assertThat( dialect ).isInstanceOf( dbType().getDialectClass() ); + assertThat( dialect.getVersion() ).isEqualTo( EXPECTED_VERSION ); + } + + assertThat( sqlTracker.getLoggedQueries() ) + .as( "No query should be executed at start up" ) + .isEmpty(); + } + + @ParameterizedTest(name = "Test {0} with " + JAKARTA_HBM2DDL_DB_NAME) + @MethodSource("explicitVersionProperties") + public void testExplicitVersionWithJakartaDbName(String display, Properties dbProperties) { + dbProperties.setProperty( ALLOW_METADATA_ON_BOOT, "false" ); + dbProperties.setProperty( JAKARTA_HBM2DDL_DB_NAME, dbType().getProductName() ); + + try (StandardServiceRegistry serviceRegistry = createServiceRegistry( dbProperties )) { + final Dialect dialect = dialect( serviceRegistry ); + assertThat( dialect ).isInstanceOf( dbType().getDialectClass() ); + assertThat( dialect.getVersion() ).isEqualTo( EXPECTED_VERSION ); + } + + assertThat( sqlTracker.getLoggedQueries() ) + .as( "No query should be executed at start up" ) + .isEmpty(); + } + + @Test + public void testMinimumDatabaseVersionWithDialect() { + final Properties dbProperties = new Properties(); + dbProperties.setProperty( ALLOW_METADATA_ON_BOOT, "false" ); + dbProperties.setProperty( DIALECT, dbType().getDialectClass().getName() ); + + try (StandardServiceRegistry serviceRegistry = createServiceRegistry( dbProperties )) { + final Dialect dialect = dialect( serviceRegistry ); + assertThat( dialect ).isInstanceOf( dbType().getDialectClass() ); + assertThat( dialect.getVersion() ).isEqualTo( dbType().getMinimumVersion() ); + } + + assertThat( sqlTracker.getLoggedQueries() ) + .as( "No query should be executed at start up" ) + .isEmpty(); + } + + @Test + public void testMinimumDatabaseVersionWithJakartaDbName() { + final Properties dbProperties = new Properties(); + dbProperties.setProperty( ALLOW_METADATA_ON_BOOT, "false" ); + dbProperties.setProperty( JAKARTA_HBM2DDL_DB_NAME, dbType().getProductName() ); + + try (StandardServiceRegistry serviceRegistry = createServiceRegistry( dbProperties )) { + final Dialect dialect = dialect( serviceRegistry ); + assertThat( dialect ).isInstanceOf( dbType().getDialectClass() ); + assertThat( dialect.getVersion() ).isEqualTo( dbType().getMinimumVersion() ); + } + + assertThat( sqlTracker.getLoggedQueries() ) + .as( "No query should be executed at start up" ) + .isEmpty(); + } + + @Test + public void testDeterminedVersion() { + final Properties disabledProperties = new Properties(); + disabledProperties.setProperty( ALLOW_METADATA_ON_BOOT, "false" ); + disabledProperties.setProperty( DIALECT, dbType().getDialectClass().getName() ); + // The dialect when ALLOW_METADATA_ON_BOOT si set to false + final Dialect metadataDisabledDialect; + try (StandardServiceRegistry serviceRegistry = createServiceRegistry( disabledProperties )) { + metadataDisabledDialect = dialect( serviceRegistry ); + // We didn't set the version anywhere else, so we expect it to be the minimum version + assertThat( metadataDisabledDialect.getVersion() ).isEqualTo( dbType().getMinimumVersion() ); + } + + assertThat( sqlTracker.getLoggedQueries() ) + .as( "No query should be executed at start up" ) + .isEmpty(); + + final Properties enabledProperties = new Properties(); + enabledProperties.setProperty( ALLOW_METADATA_ON_BOOT, "true" ); + enabledProperties.setProperty( JAKARTA_JDBC_URL, DatabaseConfiguration.getJdbcUrl() ); + try (StandardServiceRegistry serviceRegistry = createServiceRegistry( enabledProperties )) { + final Dialect metadataEnabledDialect = dialect( serviceRegistry ); + + // We expect determineDatabaseVersion(), when called on metadataAccessDisabledDialect, + // to return the version that would have been returned, + // had we booted up with auto-detection of version (metadata access allowed). + DatabaseVersion determinedDatabaseVersion = metadataDisabledDialect + .determineDatabaseVersion( dialectResolutionInfo( serviceRegistry ) ); + + // Whatever the version, we don't expect the minimum one + assertThat( determinedDatabaseVersion ).isNotEqualTo( dbType().getMinimumVersion() ); + + assertThat( determinedDatabaseVersion ).isEqualTo( metadataEnabledDialect.getVersion() ); + assertThat( determinedDatabaseVersion.getMajor() ).isEqualTo( metadataEnabledDialect.getVersion().getMajor() ); + assertThat( determinedDatabaseVersion.getMinor() ).isEqualTo( metadataEnabledDialect.getVersion().getMinor() ); + assertThat( determinedDatabaseVersion.getMicro() ).isEqualTo( metadataEnabledDialect.getVersion().getMicro() ); + } + } + + private Configuration constructConfiguration(Properties properties) { + Configuration configuration = new Configuration(); + setSqlLoggingProperties( configuration ); + configuration.addProperties( properties ); + + // Construct a tracker that collects query statements via the SqlStatementLogger framework. + // Pass in configuration properties to hand off any actual logging properties + sqlTracker = new SqlStatementTracker( s -> true, configuration.getProperties() ); + return configuration; + } + + private StandardServiceRegistry createServiceRegistry(Properties properties) { + Configuration configuration = constructConfiguration( properties ); + StandardServiceRegistryBuilder builder = new ReactiveServiceRegistryBuilder(); + // We will set these properties when needed + assertThat( builder.getSettings() ).doesNotContainKeys( DIALECT, JAKARTA_HBM2DDL_DB_NAME ); + builder.applySettings( configuration.getProperties() ); + + builder.addInitiator( new CapturingDialectFactory.Initiator() ); + sqlTracker.registerService( builder ); + return builder.enableAutoClose().build(); + } + + private static Dialect dialect(StandardServiceRegistry registry) { + return registry.getService( JdbcEnvironment.class ).getDialect(); + } + + private static DialectResolutionInfo dialectResolutionInfo(StandardServiceRegistry registry) { + return ( (CapturingDialectFactory) registry.getService( DialectFactory.class ) ) + .capturedDialectResolutionInfoSource.getDialectResolutionInfo(); + } + + // A hack to easily retrieve DialectResolutionInfo exactly as it would be constructed by Hibernate ORM + private static class CapturingDialectFactory extends DialectFactoryImpl { + + static class Initiator implements StandardServiceInitiator { + @Override + public Class getServiceInitiated() { + return DialectFactory.class; + } + + @Override + public DialectFactory initiateService(Map configurationValues, ServiceRegistryImplementor registry) { + return new CapturingDialectFactory(); + } + } + + DialectResolutionInfoSource capturedDialectResolutionInfoSource; + + @Override + public Dialect buildDialect(Map configValues, DialectResolutionInfoSource resolutionInfoSource) + throws HibernateException { + this.capturedDialectResolutionInfoSource = resolutionInfoSource; + return super.buildDialect( configValues, resolutionInfoSource ); + } + } +}