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 c30df9a285..5838943cc7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -344,7 +344,7 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun } protected Dialect(DialectResolutionInfo info) { - this.version = info.makeCopyOrDefault( getMinimumSupportedVersion() ); + this.version = determineDatabaseVersion( info ); checkVersion(); registerDefaultKeywords(); registerKeywords(info); @@ -363,6 +363,17 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun } } + /** + * Determine the database version, as precise as possible and using Dialect-specific techniques, + * from a {@link DialectResolutionInfo} object. + * @param info The dialect resolution info that would be passed by Hibernate ORM + * to the constructor of a Dialect of the same type. + * @return The corresponding database version. + */ + public DatabaseVersion determineDatabaseVersion(DialectResolutionInfo info) { + return info.makeCopyOrDefault( getMinimumSupportedVersion() ); + } + /** * Set appropriate default values for configuration properties. *

diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index dbda5ccab5..4e71a59999 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -126,7 +126,7 @@ public class H2Dialect extends Dialect { private final OptionalTableUpdateStrategy optionalTableUpdateStrategy; public H2Dialect(DialectResolutionInfo info) { - this( parseVersion( info ) ); + this( staticDetermineDatabaseVersion( info ) ); registerKeywords( info ); } @@ -151,7 +151,13 @@ public class H2Dialect extends Dialect { this.optionalTableUpdateStrategy = H2Dialect::usingMerge; } - private static DatabaseVersion parseVersion(DialectResolutionInfo info) { + @Override + public DatabaseVersion determineDatabaseVersion(DialectResolutionInfo info) { + return staticDetermineDatabaseVersion(info); + } + + // Static version necessary to call from constructor + private static DatabaseVersion staticDetermineDatabaseVersion(DialectResolutionInfo info) { DatabaseVersion version = info.makeCopyOrDefault( MINIMUM_VERSION ); if ( info.getDatabaseVersion() != null ) { version = DatabaseVersion.make( version.getMajor(), version.getMinor(), parseBuildId( info ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java index 0d21eb3fb9..f755dd5451 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -202,6 +202,10 @@ public class HANADialect extends Dialect { return MINIMUM_VERSION; } + @Override + public DatabaseVersion determineDatabaseVersion(DialectResolutionInfo info) { + return HANAServerConfiguration.staticDetermineDatabaseVersion( info ); + } // Use column or row tables by default public static final String USE_DEFAULT_TABLE_TYPE_COLUMN = "hibernate.dialect.hana.use_default_table_type_column"; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANAServerConfiguration.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANAServerConfiguration.java index ace21b79aa..8acf732190 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANAServerConfiguration.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANAServerConfiguration.java @@ -75,10 +75,10 @@ public class HANAServerConfiguration { MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE ); } - return new HANAServerConfiguration( createVersion( info ), maxLobPrefetchSize ); + return new HANAServerConfiguration( staticDetermineDatabaseVersion( info ), maxLobPrefetchSize ); } - private static DatabaseVersion createVersion(DialectResolutionInfo info) { + static DatabaseVersion staticDetermineDatabaseVersion(DialectResolutionInfo info) { // Parse the version according to https://answers.sap.com/questions/9760991/hana-sps-version-check.html final String versionString = info.getDatabaseVersion(); int majorVersion = 1; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index c98cf0806f..63962ddd88 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -174,11 +174,17 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { } public SQLServerDialect(DialectResolutionInfo info) { - this( determineDatabaseVersion( info ) ); + this( staticDetermineDatabaseVersion( info ) ); registerKeywords( info ); } - private static DatabaseVersion determineDatabaseVersion(DialectResolutionInfo info) { + @Override + public DatabaseVersion determineDatabaseVersion(DialectResolutionInfo info) { + return staticDetermineDatabaseVersion(info); + } + + // Static version necessary to call from constructor + private static DatabaseVersion staticDetermineDatabaseVersion(DialectResolutionInfo info) { final Integer compatibilityLevel = getCompatibilityLevel( info ); if ( compatibilityLevel != null ) { final int majorVersion = compatibilityLevel / 10; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SimpleDatabaseVersion.java b/hibernate-core/src/main/java/org/hibernate/dialect/SimpleDatabaseVersion.java index 7702037b02..6903fa0016 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SimpleDatabaseVersion.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SimpleDatabaseVersion.java @@ -6,6 +6,8 @@ */ package org.hibernate.dialect; +import java.util.Objects; + /** * Simple version of DatabaseVersion */ @@ -101,4 +103,21 @@ public class SimpleDatabaseVersion implements DatabaseVersion { } return version.toString(); } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + SimpleDatabaseVersion that = (SimpleDatabaseVersion) o; + return major == that.major && minor == that.minor && micro == that.micro; + } + + @Override + public int hashCode() { + return Objects.hash( major, minor, micro ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/boot/database/metadata/MetadataAccessTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/database/metadata/MetadataAccessTests.java index 02bf8327c6..1dea8f661a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/boot/database/metadata/MetadataAccessTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/database/metadata/MetadataAccessTests.java @@ -12,9 +12,11 @@ import static org.junit.jupiter.api.Assertions.fail; import java.lang.invoke.MethodHandles; import java.lang.reflect.Field; +import java.util.Map; import java.util.stream.Stream; 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.AvailableSettings; @@ -33,12 +35,18 @@ import org.hibernate.dialect.PostgresPlusDialect; import org.hibernate.dialect.SQLServerDialect; import org.hibernate.dialect.SpannerDialect; import org.hibernate.dialect.SybaseDialect; +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.internal.CoreMessageLogger; import org.hibernate.service.spi.ServiceException; +import org.hibernate.service.spi.ServiceRegistryImplementor; import org.hibernate.testing.env.TestingDatabaseInfo; import org.hibernate.testing.logger.Triggerable; +import org.hibernate.testing.orm.junit.DialectContext; import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.logger.LoggerInspectionExtension; import org.junit.jupiter.api.BeforeEach; @@ -89,8 +97,7 @@ public class MetadataAccessTests { .doesNotContainKeys( JdbcSettings.DIALECT, JdbcSettings.JAKARTA_HBM2DDL_DB_NAME ); try (StandardServiceRegistry registry = registryBuilder.build()) { - final JdbcEnvironment jdbcEnvironment = registry.getService( JdbcEnvironment.class ); - final Dialect dialect = jdbcEnvironment.getDialect(); + final Dialect dialect = getDialect( registry ); assertThat( dialect ).isNotNull(); assertThat( dialect ).isInstanceOf( H2Dialect.class ); } @@ -134,7 +141,20 @@ public class MetadataAccessTests { @ParameterizedTest @MethodSource("dialects") - void testAccessDisabledExplicitDialect(String productName, Class dialectClass, DatabaseVersion expectedDatabaseVersion) { + void testAccessDisabledExplicitDialect(String productName, Class dialectClass, + DatabaseVersion expectedDatabaseVersion) { + try ( StandardServiceRegistry registry = createRegistryWithMetadataAccessDisabledAndDialect( dialectClass ) ) { + final Dialect dialect = getDialect( registry ); + assertThat( dialect ).isInstanceOf( dialectClass ); + assertThat( dialect.getVersion() ).isEqualTo( expectedDatabaseVersion ); + } + + assertThat( triggerable.triggerMessages() ) + .as( triggerable.toString() ) + .isEmpty(); + } + + private StandardServiceRegistry createRegistryWithMetadataAccessDisabledAndDialect(Class dialectClass) { final StandardServiceRegistryBuilder registryBuilder = new StandardServiceRegistryBuilder(); registryBuilder.clearSettings(); @@ -143,16 +163,7 @@ public class MetadataAccessTests { assertThat( registryBuilder.getSettings() ) .doesNotContainKeys( JdbcSettings.JAKARTA_HBM2DDL_DB_NAME ); - try (StandardServiceRegistry registry = registryBuilder.build()) { - final JdbcEnvironment jdbcEnvironment = registry.getService( JdbcEnvironment.class ); - final Dialect dialect = jdbcEnvironment.getDialect(); - assertThat( dialect ).isInstanceOf( dialectClass ); - assertThat( dialect.getVersion() ).isEqualTo( expectedDatabaseVersion ); - } - - assertThat( triggerable.triggerMessages() ) - .as( triggerable.toString() ) - .isEmpty(); + return registryBuilder.build(); } @ParameterizedTest @@ -190,8 +201,7 @@ public class MetadataAccessTests { registryBuilder.applySetting( JdbcSettings.ALLOW_METADATA_ON_BOOT, false ); try (StandardServiceRegistry registry = registryBuilder.build()) { - final JdbcEnvironment jdbcEnvironment = registry.getService( JdbcEnvironment.class ); - final Dialect dialect = jdbcEnvironment.getDialect(); + final Dialect dialect = getDialect( registry ); fail( "Should fail to boot - " + dialect ); } catch (ServiceException expected) { @@ -201,6 +211,50 @@ public class MetadataAccessTests { } } + @Test + void testDetermineDatabaseVersion() { + final Dialect metadataAccessDisabledDialect; + try ( StandardServiceRegistry registry = + createRegistryWithMetadataAccessDisabledAndDialect( DialectContext.getDialectClass() ) ) { + // The version on this dialect may be anything, but most likely will be the minimum version. + // We're not interested in that, but in how determineDatabaseVersion() behaves for this dialect, + // when passed actual resolution info -- which Quarkus may do. + metadataAccessDisabledDialect = getDialect( registry ); + } + + try ( StandardServiceRegistry registry = createRegistryWithTestedDatabaseAndMetadataAccessAllowed() ) { + final Dialect metadataAccessAllowedDialect = getDialect( registry ); + + // 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). + assertThat( metadataAccessDisabledDialect.determineDatabaseVersion( getDialectResolutionInfo( registry ) ) ) + .isEqualTo( metadataAccessAllowedDialect.getVersion() ); + } + } + + private StandardServiceRegistry createRegistryWithTestedDatabaseAndMetadataAccessAllowed() { + final StandardServiceRegistryBuilder registryBuilder = new StandardServiceRegistryBuilder(); + + registryBuilder.addInitiator( new CapturingDialectFactory.Initiator() ); + + // allow access to the jdbc metadata + registryBuilder.applySetting( JdbcSettings.ALLOW_METADATA_ON_BOOT, true ); + + // leave connection info as defined in global test configuration (most likely system properties) + + return registryBuilder.build(); + } + + private static Dialect getDialect(StandardServiceRegistry registry) { + return registry.getService( JdbcEnvironment.class ).getDialect(); + } + + private static DialectResolutionInfo getDialectResolutionInfo(StandardServiceRegistry registry) { + return ( (CapturingDialectFactory) registry.getService( DialectFactory.class ) ) + .capturedDialectResolutionInfoSource.getDialectResolutionInfo(); + } + // Ugly hack because neither MINIMUM_VERSION nor getMinimumSupportedVersion() // can be accessed from this test. private static DatabaseVersion getVersionConstant(Class dialectClass, String versionConstantName) { @@ -213,4 +267,29 @@ public class MetadataAccessTests { throw new RuntimeException( "Error extracting '" + versionConstantName + "' from '" + dialectClass + "'", e ); } } + + // 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-testing/src/main/java/org/hibernate/testing/orm/junit/DialectContext.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectContext.java index 5146442460..abb6b769af 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectContext.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectContext.java @@ -25,40 +25,47 @@ import org.hibernate.internal.util.ReflectHelper; */ public final class DialectContext { + private static Class dialectClass; private static Dialect dialect; - static void init() { + static void initDialectClass() { final Properties properties = Environment.getProperties(); - final String diverClassName = properties.getProperty( Environment.DRIVER ); final String dialectName = properties.getProperty( Environment.DIALECT ); - final String jdbcUrl = properties.getProperty( Environment.URL ); - final Properties props = new Properties(); - props.setProperty( "user", properties.getProperty( Environment.USER ) ); - props.setProperty( "password", properties.getProperty( Environment.PASS ) ); if ( dialectName == null ) { throw new HibernateException( "The dialect was not set. Set the property hibernate.dialect." ); } - final Constructor constructor; try { - @SuppressWarnings("unchecked") - final Class dialectClass = (Class) ReflectHelper.classForName( dialectName ); - constructor = dialectClass.getConstructor( DialectResolutionInfo.class ); + dialectClass = (Class) ReflectHelper.classForName( dialectName ); } catch (ClassNotFoundException cnfe) { throw new HibernateException( "Dialect class not found: " + dialectName, cnfe ); } + } + + static void init() { + final Properties properties = Environment.getProperties(); + final String driverClassName = properties.getProperty( Environment.DRIVER ); + final String jdbcUrl = properties.getProperty( Environment.URL ); + final Properties props = new Properties(); + props.setProperty( "user", properties.getProperty( Environment.USER ) ); + props.setProperty( "password", properties.getProperty( Environment.PASS ) ); + final Class dialectClass = getDialectClass(); + final Constructor constructor; + try { + constructor = dialectClass.getConstructor( DialectResolutionInfo.class ); + } catch (Exception e) { - throw new HibernateException( "Could not instantiate given dialect class: " + dialectName, e ); + throw new HibernateException( "Could not instantiate given dialect class: " + dialectClass, e ); } final Driver driver; try { - driver = (Driver) Class.forName( diverClassName ).newInstance(); + driver = (Driver) Class.forName( driverClassName ).newInstance(); } catch (ClassNotFoundException cnfe) { - throw new HibernateException( "JDBC Driver class not found: " + dialectName, cnfe ); + throw new HibernateException( "JDBC Driver class not found: " + driverClassName, cnfe ); } catch (Exception e) { - throw new HibernateException( "Could not instantiate given JDBC driver class: " + dialectName, e ); + throw new HibernateException( "Could not instantiate given JDBC driver class: " + driverClassName, e ); } try ( Connection connection = driver.connect( jdbcUrl, props ) ) { // if ( jdbcUrl.startsWith( "jdbc:derby:" ) ) { @@ -77,15 +84,22 @@ public final class DialectContext { + jdbcUrl + "' [" + sqle.getMessage() + "]", sqle ); } catch (Exception e) { - throw new HibernateException( "Could not instantiate given dialect class: " + dialectName, e ); + throw new HibernateException( "Could not connect to database with dialect class: " + dialectClass, e ); } } private DialectContext() { } + public static synchronized Class getDialectClass() { + if ( dialectClass == null ) { + initDialectClass(); + } + return dialectClass; + } + public static synchronized Dialect getDialect() { - if (dialect==null) { + if (dialect == null) { init(); } return dialect;