Skip to content

Allow offline startup #2228

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<JdbcEnvironment> {
public class NoJdbcEnvironmentInitiator extends JdbcEnvironmentInitiator
implements StandardServiceInitiator<JdbcEnvironment> {

public static final NoJdbcEnvironmentInitiator INSTANCE = new NoJdbcEnvironmentInitiator();

Expand All @@ -42,14 +47,59 @@ public Class<JdbcEnvironment> getServiceInitiated() {
}

@Override
public JdbcEnvironment initiateService(Map<String, Object> 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<String, Object> 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<String, Object> configurationValues,
ServiceRegistryImplementor registry,
DialectFactory dialectFactory) {
return new JdbcEnvironmentImpl( registry, new DialectBuilder( configurationValues, registry )
.build( dialectFactory )
);
}

@Override
protected JdbcEnvironmentImpl getJdbcEnvironmentUsingJdbcMetadata(
Map<String, Object> 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 {
Expand All @@ -62,24 +112,40 @@ public DialectBuilder(Map<String, Object> 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<ReactiveConnection, CompletionStage<ReactiveDialectResolutionInfo>> 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<ReactiveDialectResolutionInfo> buildResolutionInfo(ReactiveConnection connection) {
final DatabaseMetadata databaseMetadata = connection.getDatabaseMetadata();
return resolutionInfoStage( connection, databaseMetadata )
return buildResolutionInfo( connection, null );
}

private static CompletionStage<ReactiveDialectResolutionInfo> buildResolutionInfo(ReactiveConnection connection, ExplicitMetadata explicitMetadata) {
return resolutionInfoStage( connection, explicitMetadata )
.handle( CompletionStages::handle )
.thenCompose( handled -> {
if ( handled.hasFailed() ) {
Expand All @@ -96,19 +162,27 @@ private static CompletionStage<ReactiveDialectResolutionInfo> buildResolutionInf
} );
}

private static CompletionStage<ReactiveDialectResolutionInfo> 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<ReactiveDialectResolutionInfo> 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 );
}

Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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();
Expand All @@ -196,6 +321,7 @@ public int getDriverMinorVersion() {

@Override
public String getSQLKeywords() {
// Vert.x metadata doesn't have this info
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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();
Expand Down
Loading
Loading