HHH-18602 Expose `determineDatabaseVersion` in `Dialect`

This commit is contained in:
Yoann Rodière 2024-09-11 19:28:18 +02:00 committed by Steve Ebersole
parent 4bd164222f
commit 4b33d0d067
8 changed files with 177 additions and 38 deletions

View File

@ -344,7 +344,7 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun
} }
protected Dialect(DialectResolutionInfo info) { protected Dialect(DialectResolutionInfo info) {
this.version = info.makeCopyOrDefault( getMinimumSupportedVersion() ); this.version = determineDatabaseVersion( info );
checkVersion(); checkVersion();
registerDefaultKeywords(); registerDefaultKeywords();
registerKeywords(info); 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. * Set appropriate default values for configuration properties.
* <p> * <p>

View File

@ -126,7 +126,7 @@ public class H2Dialect extends Dialect {
private final OptionalTableUpdateStrategy optionalTableUpdateStrategy; private final OptionalTableUpdateStrategy optionalTableUpdateStrategy;
public H2Dialect(DialectResolutionInfo info) { public H2Dialect(DialectResolutionInfo info) {
this( parseVersion( info ) ); this( staticDetermineDatabaseVersion( info ) );
registerKeywords( info ); registerKeywords( info );
} }
@ -151,7 +151,13 @@ public class H2Dialect extends Dialect {
this.optionalTableUpdateStrategy = H2Dialect::usingMerge; 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 ); DatabaseVersion version = info.makeCopyOrDefault( MINIMUM_VERSION );
if ( info.getDatabaseVersion() != null ) { if ( info.getDatabaseVersion() != null ) {
version = DatabaseVersion.make( version.getMajor(), version.getMinor(), parseBuildId( info ) ); version = DatabaseVersion.make( version.getMajor(), version.getMinor(), parseBuildId( info ) );

View File

@ -202,6 +202,10 @@ public class HANADialect extends Dialect {
return MINIMUM_VERSION; return MINIMUM_VERSION;
} }
@Override
public DatabaseVersion determineDatabaseVersion(DialectResolutionInfo info) {
return HANAServerConfiguration.staticDetermineDatabaseVersion( info );
}
// Use column or row tables by default // Use column or row tables by default
public static final String USE_DEFAULT_TABLE_TYPE_COLUMN = "hibernate.dialect.hana.use_default_table_type_column"; public static final String USE_DEFAULT_TABLE_TYPE_COLUMN = "hibernate.dialect.hana.use_default_table_type_column";

View File

@ -75,10 +75,10 @@ public class HANAServerConfiguration {
MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE 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 // Parse the version according to https://answers.sap.com/questions/9760991/hana-sps-version-check.html
final String versionString = info.getDatabaseVersion(); final String versionString = info.getDatabaseVersion();
int majorVersion = 1; int majorVersion = 1;

View File

@ -174,11 +174,17 @@ public class SQLServerDialect extends AbstractTransactSQLDialect {
} }
public SQLServerDialect(DialectResolutionInfo info) { public SQLServerDialect(DialectResolutionInfo info) {
this( determineDatabaseVersion( info ) ); this( staticDetermineDatabaseVersion( info ) );
registerKeywords( 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 ); final Integer compatibilityLevel = getCompatibilityLevel( info );
if ( compatibilityLevel != null ) { if ( compatibilityLevel != null ) {
final int majorVersion = compatibilityLevel / 10; final int majorVersion = compatibilityLevel / 10;

View File

@ -6,6 +6,8 @@
*/ */
package org.hibernate.dialect; package org.hibernate.dialect;
import java.util.Objects;
/** /**
* Simple version of DatabaseVersion * Simple version of DatabaseVersion
*/ */
@ -101,4 +103,21 @@ public class SimpleDatabaseVersion implements DatabaseVersion {
} }
return version.toString(); 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 );
}
} }

View File

@ -12,9 +12,11 @@ import static org.junit.jupiter.api.Assertions.fail;
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.Map;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.hibernate.HibernateException; import org.hibernate.HibernateException;
import org.hibernate.boot.registry.StandardServiceInitiator;
import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.AvailableSettings;
@ -33,12 +35,18 @@ import org.hibernate.dialect.PostgresPlusDialect;
import org.hibernate.dialect.SQLServerDialect; import org.hibernate.dialect.SQLServerDialect;
import org.hibernate.dialect.SpannerDialect; import org.hibernate.dialect.SpannerDialect;
import org.hibernate.dialect.SybaseDialect; 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.engine.jdbc.env.spi.JdbcEnvironment;
import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.service.spi.ServiceException; import org.hibernate.service.spi.ServiceException;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.hibernate.testing.env.TestingDatabaseInfo; import org.hibernate.testing.env.TestingDatabaseInfo;
import org.hibernate.testing.logger.Triggerable; 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.junit.Jira;
import org.hibernate.testing.orm.logger.LoggerInspectionExtension; import org.hibernate.testing.orm.logger.LoggerInspectionExtension;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -89,8 +97,7 @@ public class MetadataAccessTests {
.doesNotContainKeys( JdbcSettings.DIALECT, JdbcSettings.JAKARTA_HBM2DDL_DB_NAME ); .doesNotContainKeys( JdbcSettings.DIALECT, JdbcSettings.JAKARTA_HBM2DDL_DB_NAME );
try (StandardServiceRegistry registry = registryBuilder.build()) { try (StandardServiceRegistry registry = registryBuilder.build()) {
final JdbcEnvironment jdbcEnvironment = registry.getService( JdbcEnvironment.class ); final Dialect dialect = getDialect( registry );
final Dialect dialect = jdbcEnvironment.getDialect();
assertThat( dialect ).isNotNull(); assertThat( dialect ).isNotNull();
assertThat( dialect ).isInstanceOf( H2Dialect.class ); assertThat( dialect ).isInstanceOf( H2Dialect.class );
} }
@ -134,7 +141,20 @@ public class MetadataAccessTests {
@ParameterizedTest @ParameterizedTest
@MethodSource("dialects") @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(); final StandardServiceRegistryBuilder registryBuilder = new StandardServiceRegistryBuilder();
registryBuilder.clearSettings(); registryBuilder.clearSettings();
@ -143,16 +163,7 @@ public class MetadataAccessTests {
assertThat( registryBuilder.getSettings() ) assertThat( registryBuilder.getSettings() )
.doesNotContainKeys( JdbcSettings.JAKARTA_HBM2DDL_DB_NAME ); .doesNotContainKeys( JdbcSettings.JAKARTA_HBM2DDL_DB_NAME );
try (StandardServiceRegistry registry = registryBuilder.build()) { return 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();
} }
@ParameterizedTest @ParameterizedTest
@ -190,8 +201,7 @@ public class MetadataAccessTests {
registryBuilder.applySetting( JdbcSettings.ALLOW_METADATA_ON_BOOT, false ); registryBuilder.applySetting( JdbcSettings.ALLOW_METADATA_ON_BOOT, false );
try (StandardServiceRegistry registry = registryBuilder.build()) { try (StandardServiceRegistry registry = registryBuilder.build()) {
final JdbcEnvironment jdbcEnvironment = registry.getService( JdbcEnvironment.class ); final Dialect dialect = getDialect( registry );
final Dialect dialect = jdbcEnvironment.getDialect();
fail( "Should fail to boot - " + dialect ); fail( "Should fail to boot - " + dialect );
} }
catch (ServiceException expected) { 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() // Ugly hack because neither MINIMUM_VERSION nor getMinimumSupportedVersion()
// can be accessed from this test. // can be accessed from this test.
private static DatabaseVersion getVersionConstant(Class<? extends Dialect> dialectClass, String versionConstantName) { private static DatabaseVersion getVersionConstant(Class<? extends Dialect> dialectClass, String versionConstantName) {
@ -213,4 +267,29 @@ public class MetadataAccessTests {
throw new RuntimeException( "Error extracting '" + versionConstantName + "' from '" + dialectClass + "'", e ); 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<DialectFactory> {
@Override
public Class<DialectFactory> getServiceInitiated() {
return DialectFactory.class;
}
@Override
public DialectFactory initiateService(Map<String, Object> configurationValues,
ServiceRegistryImplementor registry) {
return new CapturingDialectFactory();
}
}
DialectResolutionInfoSource capturedDialectResolutionInfoSource;
@Override
public Dialect buildDialect(Map<String, Object> configValues, DialectResolutionInfoSource resolutionInfoSource)
throws HibernateException {
this.capturedDialectResolutionInfoSource = resolutionInfoSource;
return super.buildDialect( configValues, resolutionInfoSource );
}
}
} }

View File

@ -25,40 +25,47 @@ import org.hibernate.internal.util.ReflectHelper;
*/ */
public final class DialectContext { public final class DialectContext {
private static Class<? extends Dialect> dialectClass;
private static Dialect dialect; private static Dialect dialect;
static void init() { static void initDialectClass() {
final Properties properties = Environment.getProperties(); final Properties properties = Environment.getProperties();
final String diverClassName = properties.getProperty( Environment.DRIVER );
final String dialectName = properties.getProperty( Environment.DIALECT ); 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 ) { if ( dialectName == null ) {
throw new HibernateException( "The dialect was not set. Set the property hibernate.dialect." ); throw new HibernateException( "The dialect was not set. Set the property hibernate.dialect." );
} }
final Constructor<? extends Dialect> constructor;
try { try {
@SuppressWarnings("unchecked") dialectClass = (Class<? extends Dialect>) ReflectHelper.classForName( dialectName );
final Class<? extends Dialect> dialectClass = (Class<? extends Dialect>) ReflectHelper.classForName( dialectName );
constructor = dialectClass.getConstructor( DialectResolutionInfo.class );
} }
catch (ClassNotFoundException cnfe) { catch (ClassNotFoundException cnfe) {
throw new HibernateException( "Dialect class not found: " + dialectName, 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<? extends Dialect> dialectClass = getDialectClass();
final Constructor<? extends Dialect> constructor;
try {
constructor = dialectClass.getConstructor( DialectResolutionInfo.class );
}
catch (Exception e) { 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; final Driver driver;
try { try {
driver = (Driver) Class.forName( diverClassName ).newInstance(); driver = (Driver) Class.forName( driverClassName ).newInstance();
} }
catch (ClassNotFoundException cnfe) { 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) { 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 ) ) { try ( Connection connection = driver.connect( jdbcUrl, props ) ) {
// if ( jdbcUrl.startsWith( "jdbc:derby:" ) ) { // if ( jdbcUrl.startsWith( "jdbc:derby:" ) ) {
@ -77,15 +84,22 @@ public final class DialectContext {
+ jdbcUrl + "' [" + sqle.getMessage() + "]", sqle ); + jdbcUrl + "' [" + sqle.getMessage() + "]", sqle );
} }
catch (Exception e) { 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() { private DialectContext() {
} }
public static synchronized Class<? extends Dialect> getDialectClass() {
if ( dialectClass == null ) {
initDialectClass();
}
return dialectClass;
}
public static synchronized Dialect getDialect() { public static synchronized Dialect getDialect() {
if (dialect==null) { if (dialect == null) {
init(); init();
} }
return dialect; return dialect;