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; } 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(); 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 ); + } + } +} 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(); - } -} 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() { } - }