diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fc832e71d..c06080718 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -88,7 +88,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - db: [ 'MariaDB', 'MySQL', 'PostgreSQL', 'DB2', 'CockroachDB' ] + db: [ 'MariaDB', 'MySQL', 'PostgreSQL', 'DB2', 'CockroachDB', 'MSSQLServer' ] steps: - uses: actions/checkout@v2 - name: Get year/month for cache key diff --git a/.github/workflows/tracking-orm-5.build.yml b/.github/workflows/tracking-orm-5.build.yml index 1991a3c6a..7cfe2fca3 100644 --- a/.github/workflows/tracking-orm-5.build.yml +++ b/.github/workflows/tracking-orm-5.build.yml @@ -78,7 +78,7 @@ jobs: strategy: matrix: orm-version: [ '[5.4,5.5)','[5.5,5.6)' ] - db: ['MariaDB', 'MySQL', 'PostgreSQL', 'DB2', 'CockroachDB'] + db: ['MariaDB', 'MySQL', 'PostgreSQL', 'DB2', 'CockroachDB', 'MSSQLServer'] steps: - uses: actions/checkout@v2 - name: Set up JDK 1.8 diff --git a/.github/workflows/tracking-vertx-4.build.yml b/.github/workflows/tracking-vertx-4.build.yml index 67db7df1c..bd47aa219 100644 --- a/.github/workflows/tracking-vertx-4.build.yml +++ b/.github/workflows/tracking-vertx-4.build.yml @@ -78,7 +78,7 @@ jobs: strategy: matrix: vertx-version: [ '[4.1,4.2)' ] - db: [ 'MariaDB', 'MySQL', 'PostgreSQL', 'DB2', 'CockroachDB' ] + db: [ 'MariaDB', 'MySQL', 'PostgreSQL', 'DB2', 'CockroachDB', 'MSSQLServer' ] steps: - uses: actions/checkout@v2 - name: Set up JDK 1.8 diff --git a/README.md b/README.md index 26e4c7c8e..ec2fadfaa 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ Hibernate Reactive may be used in any plain Java program, but is especially targeted toward usage in reactive environments like [Quarkus][] and [Vert.x][]. -Currently [PostgreSQL][], [MySQL][], [MariaDB][], [Db2][], and -[CockroachDB][] are supported. +Currently [PostgreSQL][], [MySQL][], [MariaDB][], [Db2][], +[CockroachDB][] and [MS SQL Server][MSSQL] are supported. Learn more at . @@ -34,19 +34,20 @@ Hibernate Reactive has been tested with: - MariaDB 10 - Db2 11.5 - CockroachDB 21.1 -- [Hibernate ORM][] 5.5.3.Final -- [Vert.x Reactive PostgreSQL Client](https://vertx.io/docs/vertx-pg-client/java/) 4.1.1 -- [Vert.x Reactive MySQL Client](https://vertx.io/docs/vertx-mysql-client/java/) 4.1.1 -- [Vert.x Reactive Db2 Client](https://vertx.io/docs/vertx-db2-client/java/) 4.1.1 +- MS SQL Server 2019 +- [Hibernate ORM][] 5.5.4.Final +- [Vert.x Reactive PostgreSQL Client](https://vertx.io/docs/vertx-pg-client/java/) 4.1.2 +- [Vert.x Reactive MySQL Client](https://vertx.io/docs/vertx-mysql-client/java/) 4.1.2 +- [Vert.x Reactive Db2 Client](https://vertx.io/docs/vertx-db2-client/java/) 4.1.2 +- [Vert.x Reactive MS SQL Server Client](https://vertx.io/docs/vertx-mssql-client/java/) 4.1.2 - [Quarkus][Quarkus] via the Hibernate Reactive extension -Support for SQL Server is coming soon. - [PostgreSQL]: https://www.postgresql.org [MySQL]: https://www.mysql.com [MariaDB]: https://mariadb.com [DB2]: https://www.ibm.com/analytics/db2 [CockroachDB]: https://www.cockroachlabs.com/ +[MSSQL]: https://www.microsoft.com/en-gb/sql-server ## Documentation diff --git a/build.gradle b/build.gradle index efd97c86b..b5ec478f5 100644 --- a/build.gradle +++ b/build.gradle @@ -53,7 +53,7 @@ version = projectVersion // ./gradlew clean build -PhibernateOrmVersion=5.5.1-SNAPSHOT ext { if ( !project.hasProperty('hibernateOrmVersion') ) { - hibernateOrmVersion = '5.5.3.Final' + hibernateOrmVersion = '5.5.4.Final' } // For ORM, we need a parsed version (to get the family, ...) @@ -79,7 +79,7 @@ ext { // Example: // ./gradlew build -PvertxVersion=4.0.0-SNAPSHOT if ( !project.hasProperty('vertxVersion') ) { - vertxVersion = '4.1.1' + vertxVersion = '4.1.2' } testcontainersVersion = '1.15.3' diff --git a/gradle.properties b/gradle.properties index 25d7a3b6d..4651d91e2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -45,5 +45,5 @@ org.gradle.java.installations.auto-download=false #skipOrmVersionParsing = true # Override default Vert.x version -#vertxVersion = 4.1.1-SNAPSHOT +#vertxVersion = 4.1.2-SNAPSHOT diff --git a/hibernate-reactive-core/build.gradle b/hibernate-reactive-core/build.gradle index 3141242cb..d6d173fdb 100644 --- a/hibernate-reactive-core/build.gradle +++ b/hibernate-reactive-core/build.gradle @@ -37,10 +37,13 @@ dependencies { testImplementation "io.vertx:vertx-pg-client:${vertxVersion}" testImplementation "io.vertx:vertx-mysql-client:${vertxVersion}" testImplementation "io.vertx:vertx-db2-client:${vertxVersion}" + testImplementation "io.vertx:vertx-mssql-client:${vertxVersion}" // JDBC driver to test with ORM and PostgreSQL testRuntimeOnly "org.postgresql:postgresql:42.2.23" + // JDBC driver for Testcontainers with MS SQL Server + testRuntimeOnly "com.microsoft.sqlserver:mssql-jdbc:9.2.1.jre8" // EHCache testRuntimeOnly "org.ehcache:ehcache:3.8.1" testRuntimeOnly ("org.hibernate:hibernate-jcache:${hibernateOrmVersion}") { @@ -59,6 +62,7 @@ dependencies { testImplementation "org.testcontainers:mariadb:${testcontainersVersion}" testImplementation "org.testcontainers:db2:${testcontainersVersion}" testImplementation "org.testcontainers:cockroachdb:${testcontainersVersion}" + testImplementation "org.testcontainers:mssqlserver:${testcontainersVersion}" } // Print a summary of the results of the tests (number of failures, successes and skipped) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/adaptor/impl/QueryParametersAdaptor.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/adaptor/impl/QueryParametersAdaptor.java index e6cb2ee47..25d749f8e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/adaptor/impl/QueryParametersAdaptor.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/adaptor/impl/QueryParametersAdaptor.java @@ -5,6 +5,7 @@ */ package org.hibernate.reactive.adaptor.impl; +import org.hibernate.dialect.pagination.LimitHandler; import org.hibernate.engine.spi.QueryParameters; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.param.ParameterSpecification; @@ -18,7 +19,8 @@ private QueryParametersAdaptor() { } public static Object[] arguments(QueryParameters queryParameters, - SharedSessionContractImplementor session) { + SharedSessionContractImplementor session, + LimitHandler limitHandler) { return PreparedStatementAdaptor.bind( adaptor -> { Type[] types = queryParameters.getFilteredPositionalParameterTypes(); Object[] values = queryParameters.getFilteredPositionalParameterValues(); @@ -29,6 +31,8 @@ public static Object[] arguments(QueryParameters queryParameters, type.nullSafeSet( adaptor, value, pos, session ); pos += type.getColumnSpan( session.getFactory() ); } + + pos += limitHandler.bindLimitParametersAtEndOfQuery( queryParameters.getRowSelection(), adaptor, pos ); } ); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bulk/StatementsWithParameters.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bulk/StatementsWithParameters.java index d1a514d08..90dcd082f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bulk/StatementsWithParameters.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bulk/StatementsWithParameters.java @@ -5,7 +5,14 @@ */ package org.hibernate.reactive.bulk; +import java.util.concurrent.CompletionStage; + +import org.hibernate.engine.spi.QueryParameters; import org.hibernate.param.ParameterSpecification; +import org.hibernate.reactive.adaptor.impl.QueryParametersAdaptor; +import org.hibernate.reactive.session.ReactiveQueryExecutor; + +import static org.hibernate.reactive.util.impl.CompletionStages.total; /** * A list of SQL statements to be executed as a single logical unit. @@ -26,22 +33,16 @@ public interface StatementsWithParameters { ParameterSpecification[][] getParameterSpecifications(); /** - * Is the given statement executed inside the current transaction? - * - * @return true by default - */ - default boolean isTransactionalStatement(String statement) { - return true; - } - - /** - * Should the result of this statement contribute to the running - * updated row count? - * - * @return false for DDL statements by default + * Execute the statements using the query parameters */ - default boolean isSchemaDefinitionStatement(String statement) { - return statement.startsWith("create ") - || statement.startsWith("drop "); + default CompletionStage execute(ReactiveQueryExecutor session, QueryParameters queryParameters) { + return total( 0, getSqlStatements().length, i -> { + final Object[] arguments = QueryParametersAdaptor.arguments( + queryParameters, + getParameterSpecifications()[i], + session.getSharedContract() + ); + return session.getReactiveConnection().update( getSqlStatements()[i], arguments ); + } ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bulk/impl/ReactiveBulkIdStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bulk/impl/ReactiveBulkIdStrategy.java index 132196cee..d7713343f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bulk/impl/ReactiveBulkIdStrategy.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bulk/impl/ReactiveBulkIdStrategy.java @@ -5,6 +5,14 @@ */ package org.hibernate.reactive.bulk.impl; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.stream.Collectors; + import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.spi.MetadataBuildingOptions; import org.hibernate.boot.spi.MetadataImplementor; @@ -19,6 +27,7 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.internal.ast.tree.AbstractRestrictableStatement; import org.hibernate.hql.internal.ast.tree.AssignmentSpecification; import org.hibernate.hql.internal.ast.tree.DeleteStatement; import org.hibernate.hql.internal.ast.tree.FromElement; @@ -29,6 +38,8 @@ import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; import org.hibernate.hql.spi.id.global.GlobalTemporaryTableBulkIdStrategy; import org.hibernate.hql.spi.id.local.IdTableInfoImpl; +import org.hibernate.internal.CoreLogging; +import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Table; @@ -39,18 +50,15 @@ import org.hibernate.reactive.pool.ReactiveConnection; import org.hibernate.reactive.pool.ReactiveConnectionPool; import org.hibernate.reactive.pool.impl.Parameters; +import org.hibernate.reactive.session.ReactiveQueryExecutor; import org.hibernate.reactive.util.impl.CompletionStages; import org.hibernate.sql.Delete; import org.hibernate.sql.Update; import org.hibernate.type.CollectionType; import org.hibernate.type.Type; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; +import static org.hibernate.reactive.util.impl.CompletionStages.loop; +import static org.hibernate.reactive.util.impl.CompletionStages.zeroFuture; /** * A reactive version of {@link AbstractMultiTableBulkIdStrategyImpl} used @@ -65,11 +73,13 @@ public class ReactiveBulkIdStrategy extends AbstractMultiTableBulkIdStrategyImpl implements MultiTableBulkIdStrategy { + private static final CoreMessageLogger LOG = CoreLogging.messageLogger( ReactiveBulkIdStrategy.class ); + private static final ParameterSpecification[] NO_PARAMS = new ParameterSpecification[0]; private final boolean db2; private final Set createdGlobalTemporaryTables = new HashSet<>(); - private final List dropTableStatements = new ArrayList<>(); + private final List dropGlobalTemporaryTables = new ArrayList<>(); private final Parameters parameters; private StandardServiceRegistry serviceRegistry; @@ -112,7 +122,7 @@ protected IdTableInfoImpl buildIdTableInfo( @Override public void release(JdbcServices jdbcServices, JdbcConnectionAccess connectionAccess) { - if ( serviceRegistry!=null && !dropTableStatements.isEmpty() ) { + if ( serviceRegistry != null && !dropGlobalTemporaryTables.isEmpty() ) { boolean dropIdTables = serviceRegistry.getService( ConfigurationService.class ) .getSetting( GlobalTemporaryTableBulkIdStrategy.DROP_ID_TABLES, @@ -120,12 +130,11 @@ public void release(JdbcServices jdbcServices, JdbcConnectionAccess connectionAc false ); if ( dropIdTables ) { - ReactiveConnection connection = - serviceRegistry.getService( ReactiveConnectionPool.class ) - .getProxyConnection(); - CompletionStages.loop( dropTableStatements, connection::execute ) + ReactiveConnection connection = serviceRegistry.getService( ReactiveConnectionPool.class ) + .getProxyConnection(); + loop( dropGlobalTemporaryTables, connection::execute ) .whenComplete( (v, e) -> connection.close() ) - .handle( (v, e) -> null ) //ignore errors + .handle( CompletionStages::ignoreErrors ) .toCompletableFuture().join(); } } @@ -133,46 +142,236 @@ public void release(JdbcServices jdbcServices, JdbcConnectionAccess connectionAc @Override public UpdateHandler buildUpdateHandler(SessionFactoryImplementor factory, HqlSqlWalker walker) { - return new TableBasedUpdateHandlerImpl( factory, walker ); + return new TableBasedUpdateHandlerImpl( factory, walker, targetedPersister( walker ) ); } @Override public DeleteHandler buildDeleteHandler(SessionFactoryImplementor factory, HqlSqlWalker walker) { - return new TableBasedDeleteHandlerImpl( factory, walker ); + return new TableBasedDeleteHandlerImpl( factory, walker, targetedPersister( walker ) ); + } + + private Queryable targetedPersister(HqlSqlWalker walker) { + AbstractRestrictableStatement restrictableStatement = (AbstractRestrictableStatement) walker.getAST(); + FromElement fromElement = restrictableStatement.getFromClause().getFromElement(); + return fromElement.getQueryable(); + } + + /** + * Different databases creates temporary tables + * in different ways and they have different requirements + * about how to run those queries. + */ + private interface TempTableStatementsExecutor { + CompletionStage createTempTable(); + + CompletionStage dropTempTable(Integer currentTotal); + + /** + * Mainly for keeping track which statement has failed + */ + String getFailedStatement(); + + /** + * Ignore errors, since a create + * table fails whenever the table already exists + * or a drop table might fail if the table doesn't exist + */ + default Integer ignoreException(Void unused, Throwable throwable) { + if ( throwable != null ) { + LOG.debugf( "Statement '%s' failed. Ignoring the exception: %s", getFailedStatement(), throwable.getMessage() ); + } + return 0; + } } - private class TableBasedUpdateHandlerImpl extends AbstractTableBasedBulkIdHandler - implements MultiTableBulkIdStrategy.UpdateHandler, StatementsWithParameters { + /** + * Execute the queries for the creation and drop of local temporary tables. + * For some databases (MSSQL for example) the query for the creation of the table must run in + * a non prepared query. + */ + private static class LocalTempTableStatementsExecutor implements TempTableStatementsExecutor { + private final String createStatement; + private final String dropStatement; + private final ReactiveQueryExecutor session; + private String failedStatement; + + private LocalTempTableStatementsExecutor(IdTableInfoImpl tableInfo, ReactiveQueryExecutor session) { + this.session = session; + createStatement = tableInfo.getIdTableCreationStatement(); + dropStatement = tableInfo.getIdTableDropStatement(); + } + + public CompletionStage createTempTable() { + failedStatement = createStatement; + return session.getReactiveConnection() + .executeUnprepared( createStatement ) + .handle( this::ignoreException ); + } + + public CompletionStage dropTempTable(Integer total) { + failedStatement = dropStatement; + return session.getReactiveConnection() + .execute( dropStatement ) + .handle( this::ignoreException ) + .thenApply( zero -> total ); + } + + @Override + public String getFailedStatement() { + return failedStatement; + } + } + + /** + * Db2 uses global temporary tables + */ + private class Db2TempTableStatementsExecutor implements TempTableStatementsExecutor { + + private final String dropStatement; + private final String createStatement; + private final String deleteStatement; + private final ReactiveQueryExecutor session; + private String failedStatement; + + private Db2TempTableStatementsExecutor(IdTableInfoImpl tableInfo, ReactiveQueryExecutor session) { + this.session = session; + if ( createdGlobalTemporaryTables.add( tableInfo.getQualifiedIdTableName() ) ) { + dropStatement = tableInfo.getIdTableDropStatement(); + createStatement = tableInfo.getIdTableCreationStatement(); + dropGlobalTemporaryTables.add( tableInfo.getIdTableDropStatement() ); + } + else { + // The global temp table should already exist + createStatement = null; + dropStatement = null; + } + deleteStatement = getIdTableSupport().getTruncateIdTableCommand() + " " + tableInfo.getQualifiedIdTableName(); + } + + public CompletionStage createTempTable() { + if ( createStatement == null ) { + return zeroFuture(); + } + return executeOutside( session, dropStatement ) + .thenCompose( integer -> executeOutside( session, createStatement ) ); + } + + private CompletionStage executeOutside(ReactiveQueryExecutor session, String sql) { + failedStatement = sql; + return session.getReactiveConnection() + .executeOutsideTransaction( sql ) + .handle( this::ignoreException ); + } + + public CompletionStage dropTempTable(Integer total) { +// if ( useSessionIdColumn() ) { +// Delete drop = new Delete() +// .setTableName( tableInfo.getQualifiedIdTableName() ) +// .setWhere( SESSION_ID_COLUMN_NAME + "=?" ); +// statements.add( drop.toStatementString() ); +// parameterSpecifications.add( new ParameterSpecification[] { SESSION_ID } ); +// } + return session.getReactiveConnection() + .execute( deleteStatement ) + .handle( this::ignoreException ); + } + + @Override + public String getFailedStatement() { + return failedStatement; + } + } + + /** + * Handle the statements for the creation and drop of temporary tables. They sometimes need to run + * as query instead of preparedQuery (it depends on the database). + */ + private abstract class TempTableHandler extends AbstractTableBasedBulkIdHandler + implements StatementsWithParameters { + + private final Queryable targetedPersister; + + public TempTableHandler(SessionFactoryImplementor sessionFactory, HqlSqlWalker walker, Queryable targetedPersister) { + super( sessionFactory, walker ); + this.targetedPersister = targetedPersister; + } + + @Override + public Queryable getTargetedQueryable() { + return targetedPersister; + } + + @Override + public CompletionStage execute(ReactiveQueryExecutor session, QueryParameters queryParameters) { + TempTableStatementsExecutor statementsExecutor = createStatementsExecutor( session ); + return statementsExecutor.createTempTable() + .thenCompose( zero -> StatementsWithParameters.super.execute( session, queryParameters ) ) + .thenCompose( statementsExecutor::dropTempTable ); + } + + private TempTableStatementsExecutor createStatementsExecutor(ReactiveQueryExecutor session) { + return db2 + ? new Db2TempTableStatementsExecutor( getIdTableInfo( targetedPersister ), session ) + : new LocalTempTableStatementsExecutor( getIdTableInfo( targetedPersister ), session ); + } + + @Override + protected String generateIdInsertSelect(String tableAlias, IdTableInfo idTableInfo, ProcessedWhereClause whereClause) { + String sql = super.generateIdInsertSelect( tableAlias, idTableInfo, whereClause ); + return parameters.process( sql ); + } + + @Override + protected String generateIdSubselect(Queryable persister, AbstractCollectionPersister cPersister, IdTableInfo idTableInfo) { + String sql = super.generateIdSubselect( persister, cPersister, idTableInfo ); + return parameters.process( sql ); + } + + @Override + protected String generateIdSubselect(Queryable persister, IdTableInfo idTableInfo) { + String sql = super.generateIdSubselect( persister, idTableInfo ); + return parameters.process( sql ); + } + + @Override + protected void prepareForUse(Queryable persister, SharedSessionContractImplementor session) { + throw new UnsupportedOperationException(); + } + + @Override + protected void releaseFromUse(Queryable persister, SharedSessionContractImplementor session) { + throw new UnsupportedOperationException(); + } + } + + private class TableBasedUpdateHandlerImpl extends TempTableHandler + implements MultiTableBulkIdStrategy.UpdateHandler { private final String[] statements; private final ParameterSpecification[][] parameterSpecifications; - private final Queryable targetedPersister; @Override public ParameterSpecification[][] getParameterSpecifications() { return parameterSpecifications; } - public TableBasedUpdateHandlerImpl(SessionFactoryImplementor factory, HqlSqlWalker walker) { - super( factory, walker ); - - ArrayList statements = new ArrayList<>(); - ArrayList parameterSpecifications = new ArrayList<>(); + public TableBasedUpdateHandlerImpl(SessionFactoryImplementor factory, HqlSqlWalker walker, Queryable targetedPersister) { + super( factory, walker,targetedPersister ); UpdateStatement updateStatement = (UpdateStatement) walker.getAST(); FromElement fromElement = updateStatement.getFromClause().getFromElement(); String bulkTargetAlias = fromElement.getTableAlias(); - targetedPersister = fromElement.getQueryable(); IdTableInfoImpl tableInfo = getIdTableInfo( targetedPersister ); List assignments = walker.getAssignmentSpecifications(); String[] tableNames = targetedPersister.getConstraintOrderedTableNameClosure(); String[][] columnNames = targetedPersister.getContraintOrderedTableKeyColumnClosure(); - createTempTable( statements, parameterSpecifications, tableInfo ); - ProcessedWhereClause processedWhereClause = processWhereClause( updateStatement.getWhereClause() ); String idInsertSelect = generateIdInsertSelect( bulkTargetAlias, tableInfo, processedWhereClause ); + + List statements = new ArrayList<>(); + List parameterSpecifications = new ArrayList<>(); statements.add( idInsertSelect ); // if ( useSessionIdColumn() ) { // specifications.add( SESSION_ID ); @@ -208,60 +407,18 @@ public TableBasedUpdateHandlerImpl(SessionFactoryImplementor factory, HqlSqlWalk } } - dropTempTable( statements, parameterSpecifications, tableInfo ); - this.statements = ArrayHelper.toStringArray( statements ); this.parameterSpecifications = parameterSpecifications.toArray( new ParameterSpecification[0][] ); } - @Override - protected String generateIdInsertSelect( - String tableAlias, IdTableInfo idTableInfo, ProcessedWhereClause whereClause) { - String sql = super.generateIdInsertSelect( tableAlias, idTableInfo, whereClause ); - return parameters.process( sql ); - } - - @Override - protected String generateIdSubselect( - Queryable persister, AbstractCollectionPersister cPersister, IdTableInfo idTableInfo) { - String sql = super.generateIdSubselect( persister, cPersister, idTableInfo ); - return parameters.process( sql ); - } - - @Override - protected String generateIdSubselect(Queryable persister, IdTableInfo idTableInfo) { - String sql = super.generateIdSubselect( persister, idTableInfo ); - return parameters.process( sql ); - } - - @Override - public Queryable getTargetedQueryable() { - return targetedPersister; - } - - @Override - public String[] getSqlStatements() { - return statements; - } - - @Override - public boolean isTransactionalStatement(String statement) { - return !db2 || !isSchemaDefinitionStatement(statement); - } - @Override public int execute(SharedSessionContractImplementor session, QueryParameters queryParameters) { throw new UnsupportedOperationException(); } @Override - protected void prepareForUse(Queryable persister, SharedSessionContractImplementor session) { - throw new UnsupportedOperationException(); - } - - @Override - protected void releaseFromUse(Queryable persister, SharedSessionContractImplementor session) { - throw new UnsupportedOperationException(); + public String[] getSqlStatements() { + return statements; } // @Override @@ -278,33 +435,28 @@ protected void releaseFromUse(Queryable persister, SharedSessionContractImplemen // } } - private class TableBasedDeleteHandlerImpl extends AbstractTableBasedBulkIdHandler - implements MultiTableBulkIdStrategy.DeleteHandler, StatementsWithParameters { + private class TableBasedDeleteHandlerImpl extends TempTableHandler + implements MultiTableBulkIdStrategy.DeleteHandler { - private final Queryable targetedPersister; - - private final ParameterSpecification[][] parameterSpecifications; private final String[] statements; + private final ParameterSpecification[][] parameterSpecifications; @Override public ParameterSpecification[][] getParameterSpecifications() { return parameterSpecifications; } - public TableBasedDeleteHandlerImpl(SessionFactoryImplementor factory, HqlSqlWalker walker) { - super( factory, walker ); + public TableBasedDeleteHandlerImpl(SessionFactoryImplementor factory, HqlSqlWalker walker, Queryable targetedPersister) { + super( factory, walker, targetedPersister ); - ArrayList statements = new ArrayList<>(); - ArrayList parameterSpecifications = new ArrayList<>(); + List statements = new ArrayList<>(); + List parameterSpecifications = new ArrayList<>(); DeleteStatement deleteStatement = (DeleteStatement) walker.getAST(); FromElement fromElement = deleteStatement.getFromClause().getFromElement(); String bulkTargetAlias = fromElement.getTableAlias(); - targetedPersister = fromElement.getQueryable(); IdTableInfoImpl tableInfo = getIdTableInfo( targetedPersister ); - createTempTable( statements, parameterSpecifications, tableInfo ); - ProcessedWhereClause processedWhereClause = processWhereClause( deleteStatement.getWhereClause() ); String idInsertSelect = generateIdInsertSelect( bulkTargetAlias, tableInfo, processedWhereClause ); statements.add( idInsertSelect ); @@ -351,45 +503,15 @@ public TableBasedDeleteHandlerImpl(SessionFactoryImplementor factory, HqlSqlWalk // parameterSpecifications.add( useSessionIdColumn() ? new ParameterSpecification[] { SESSION_ID } : NO_PARAMS ); } - dropTempTable( statements, parameterSpecifications, tableInfo ); - this.statements = ArrayHelper.toStringArray( statements ); this.parameterSpecifications = parameterSpecifications.toArray( new ParameterSpecification[0][] ); } - @Override - protected String generateIdInsertSelect(String tableAlias, IdTableInfo idTableInfo, ProcessedWhereClause whereClause) { - String sql = super.generateIdInsertSelect( tableAlias, idTableInfo, whereClause ); - return parameters.process( sql ); - } - - @Override - protected String generateIdSubselect(Queryable persister, IdTableInfo idTableInfo) { - String sql = super.generateIdSubselect( persister, idTableInfo ); - return parameters.process( sql ); - } - - @Override - protected String generateIdSubselect(Queryable persister, AbstractCollectionPersister cPersister, IdTableInfo idTableInfo) { - String sql = super.generateIdSubselect( persister, cPersister, idTableInfo ); - return parameters.process( sql ); - } - - @Override - public Queryable getTargetedQueryable() { - return targetedPersister; - } - @Override public String[] getSqlStatements() { return statements; } - @Override - public boolean isTransactionalStatement(String statement) { - return !db2 || !isSchemaDefinitionStatement( statement ); - } - @Override public int execute(SharedSessionContractImplementor session, QueryParameters queryParameters) { throw new UnsupportedOperationException(); @@ -416,44 +538,6 @@ public int execute(SharedSessionContractImplementor session, QueryParameters que // } } - private void createTempTable(ArrayList statements, - ArrayList parameterSpecifications, - IdTableInfoImpl tableInfo) { - if (db2) { - if ( createdGlobalTemporaryTables.add( tableInfo.getQualifiedIdTableName() ) ) { - statements.add( tableInfo.getIdTableDropStatement() ); - parameterSpecifications.add( NO_PARAMS ); - statements.add( tableInfo.getIdTableCreationStatement() ); - parameterSpecifications.add( NO_PARAMS ); - dropTableStatements.add( tableInfo.getIdTableDropStatement() ); - } - } - else { - statements.add( tableInfo.getIdTableCreationStatement() ); - parameterSpecifications.add( NO_PARAMS ); - } - } - - private void dropTempTable(ArrayList statements, - ArrayList parameterSpecifications, - IdTableInfoImpl tableInfo) { -// if ( useSessionIdColumn() ) { -// Delete drop = new Delete() -// .setTableName( tableInfo.getQualifiedIdTableName() ) -// .setWhere( SESSION_ID_COLUMN_NAME + "=?" ); -// statements.add( drop.toStatementString() ); -// parameterSpecifications.add( new ParameterSpecification[] { SESSION_ID } ); -// } -// else { - if (db2) { - statements.add( getIdTableSupport().getTruncateIdTableCommand() + " " + tableInfo.getQualifiedIdTableName() ); - } - else { - statements.add( tableInfo.getIdTableDropStatement() ); - } - parameterSpecifications.add( NO_PARAMS ); - } - // @Override // protected void augmentIdTableDefinition(Table idTable) { // if ( useSessionIdColumn() ) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bulk/impl/ReactiveIdTableSupport.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bulk/impl/ReactiveIdTableSupport.java index 89731abc7..1872e4c4a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bulk/impl/ReactiveIdTableSupport.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bulk/impl/ReactiveIdTableSupport.java @@ -10,6 +10,7 @@ import org.hibernate.dialect.MariaDB103Dialect; import org.hibernate.dialect.MySQL8Dialect; import org.hibernate.dialect.PostgreSQL10Dialect; +import org.hibernate.dialect.SQLServerDialect; import org.hibernate.hql.spi.id.IdTableSupportStandardImpl; /** @@ -28,7 +29,7 @@ public ReactiveIdTableSupport(Dialect dialect) { @Override public String generateIdTableName(String baseName) { - return "ht_" + baseName; + return (dialect instanceof SQLServerDialect ? "#" : "ht_") + baseName; } @Override @@ -42,6 +43,9 @@ else if (dialect instanceof MySQL8Dialect || dialect instanceof MariaDB103Dialec else if (dialect instanceof DB297Dialect) { return "create global temporary table"; } + else if (dialect instanceof SQLServerDialect) { + return "create table"; + } else { return "create local temporary table"; } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/CachingReactiveLoader.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/CachingReactiveLoader.java index 6b5bf3eca..bd66a06df 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/CachingReactiveLoader.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/CachingReactiveLoader.java @@ -182,11 +182,11 @@ default CacheableResultTransformer cacheableResultTransformer(QueryParameters qu List getResultList(List results, ResultTransformer resultTransformer) throws QueryException; @Override - default Object[] toParameterArray(QueryParameters queryParameters, SharedSessionContractImplementor session) { + default Object[] toParameterArray(QueryParameters queryParameters, SharedSessionContractImplementor session, LimitHandler limitHandler) { return PreparedStatementAdaptor.bind( adaptor -> bindToPreparedStatement( adaptor, queryParameters, - limitHandler( queryParameters.getRowSelection(), session ), + limitHandler, session ) ); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ReactiveLoader.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ReactiveLoader.java index 62b09457f..599673e90 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ReactiveLoader.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ReactiveLoader.java @@ -109,8 +109,7 @@ default CompletionStage executeReactiveQueryStatement( // Adding locks and comments. sql = preprocessSQL( sql, queryParameters, session.getFactory(), afterLoadActions ); - - Object[] parameterArray = toParameterArray( queryParameters, session ); + Object[] parameterArray = toParameterArray( queryParameters, session, limitHandler ); boolean hasFilter = session.getLoadQueryInfluencers().hasEnabledFilters(); if ( hasFilter ) { @@ -179,7 +178,7 @@ default String preprocessSQL(String sql, QueryParameters queryParameters, Sessio */ default void discoverTypes(QueryParameters queryParameters, ResultSet resultSet) {} - default Object[] toParameterArray(QueryParameters queryParameters, SharedSessionContractImplementor session) { - return QueryParametersAdaptor.arguments( queryParameters, session ); + default Object[] toParameterArray(QueryParameters queryParameters, SharedSessionContractImplementor session, LimitHandler limitHandler) { + return QueryParametersAdaptor.arguments( queryParameters, session, limitHandler ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractEntityPersister.java index c771f38ce..60a6a8b3a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractEntityPersister.java @@ -26,10 +26,7 @@ import org.hibernate.StaleObjectStateException; import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeDescriptor; import org.hibernate.collection.spi.PersistentCollection; -import org.hibernate.dialect.CockroachDB192Dialect; -import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.PostgreSQL81Dialect; import org.hibernate.engine.OptimisticLockStyle; import org.hibernate.engine.internal.Versioning; import org.hibernate.engine.spi.EntityEntry; @@ -102,6 +99,7 @@ * @see ReactiveSingleTableEntityPersister */ public interface ReactiveAbstractEntityPersister extends ReactiveEntityPersister, OuterJoinLoadable, Lockable { + Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() ); default Parameters parameters() { @@ -368,7 +366,7 @@ default CompletionStage insertReactive( // Ignoring it for now because it's always false and we have ways to get the id without // the extra round trip for all supported databases // if ( getFactory().getSessionFactoryOptions().isGetGeneratedKeysEnabled() ) { - generatedIdStage = connection.insertAndSelectIdentifier( checkSql( sql ), params ); + generatedIdStage = connection.insertAndSelectIdentifier( sql, params ); // } // else { // //use an extra round trip to fetch the id @@ -385,22 +383,6 @@ default CompletionStage insertReactive( } ); } - /** - * Queries used to insert a new element and retrieve the id in one go require - * some changes - */ - default String checkSql(String sql) { - Dialect dialect = getFactory().getJdbcServices().getDialect(); - //TODO: wooooo this is awful ... I believe the problem is fixed in Hibernate 6 - if ( dialect instanceof PostgreSQL81Dialect || dialect instanceof CockroachDB192Dialect ) { - return sql + " returning " + delegate().getIdentifierColumnNames()[0]; - } - if ( dialect instanceof DB2Dialect ) { - return "select " + delegate().getIdentifierColumnNames()[0] + " from NEW TABLE (" + sql + ")"; - } - return sql; - } - default CompletionStage deleteReactive( Serializable id, Object version, diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveIdentityGenerator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveIdentityGenerator.java new file mode 100644 index 000000000..04699f405 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveIdentityGenerator.java @@ -0,0 +1,173 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.persister.entity.impl; + +import java.util.Iterator; + +import org.hibernate.HibernateException; +import org.hibernate.MappingException; +import org.hibernate.dialect.CockroachDB192Dialect; +import org.hibernate.dialect.DB2Dialect; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.PostgreSQL81Dialect; +import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.id.IdentityGenerator; +import org.hibernate.id.PostInsertIdentityPersister; +import org.hibernate.id.insert.IdentifierGeneratingInsert; +import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate; +import org.hibernate.sql.Insert; + +/** + * Fix the insert and select id queries generated by Hibernate ORM + */ +public class ReactiveIdentityGenerator extends IdentityGenerator { + + @Override + public InsertGeneratedIdentifierDelegate getInsertGeneratedIdentifierDelegate( + PostInsertIdentityPersister persister, Dialect dialect, boolean isGetGeneratedKeysEnabled) + throws HibernateException { + return new ReactiveInsertAndSelectDelegate( persister, dialect ); + } + + public static class ReactiveInsertAndSelectDelegate extends InsertSelectDelegate { + + private final PostInsertIdentityPersister persister; + private final Dialect dialect; + + public ReactiveInsertAndSelectDelegate(PostInsertIdentityPersister persister, Dialect dialect) { + super( persister, dialect ); + this.persister = persister; + this.dialect = dialect; + } + + @Override + public IdentifierGeneratingInsert prepareIdentifierGeneratingInsert() { + IdentifierGeneratingInsert insert = createInsert(); + insert.addIdentityColumn( persister.getRootTableKeyColumnNames()[0] ); + return insert; + } + + private IdentifierGeneratingInsert createInsert() { + if ( dialect instanceof PostgreSQL81Dialect || dialect instanceof CockroachDB192Dialect ) { + return new PostgresIdentifierGeneratingInsert( dialect ); + } + if ( dialect instanceof SQLServerDialect ) { + return new SqlServerIdentifierGeneratingInsert( dialect ); + } + if ( dialect instanceof DB2Dialect ) { + return new Db2IdentifierGeneratingInsert( dialect ); + } + return super.prepareIdentifierGeneratingInsert(); + } + } + + public static class Db2IdentifierGeneratingInsert extends IdentifierGeneratingInsert { + + private String identityColumnName; + + public Db2IdentifierGeneratingInsert(Dialect dialect) { + super( dialect ); + } + + @Override + public Insert addIdentityColumn(String columnName) { + this.identityColumnName = columnName; + return super.addIdentityColumn( columnName ); + } + + /** + * @see Insert#toStatementString() + */ + @Override + public String toStatementString() { + return "select " + identityColumnName + " from NEW TABLE (" + super.toStatementString() + ")"; + } + } + + public static class PostgresIdentifierGeneratingInsert extends IdentifierGeneratingInsert { + + private String identityColumnName; + + public PostgresIdentifierGeneratingInsert(Dialect dialect) { + super( dialect ); + } + + @Override + public Insert addIdentityColumn(String columnName) { + this.identityColumnName = columnName; + return super.addIdentityColumn( columnName ); + } + + @Override + public String toStatementString() { + return super.toStatementString() + " returning " + identityColumnName; + } + } + + public static class SqlServerIdentifierGeneratingInsert extends IdentifierGeneratingInsert { + private String identityColumnName; + + public SqlServerIdentifierGeneratingInsert(Dialect dialect) { + super( dialect ); + } + + @Override + public Insert addIdentityColumn(String columnName) { + this.identityColumnName = columnName; + return super.addIdentityColumn( columnName ); + } + + /** + * @see Insert#toStatementString() + */ + public String toStatementString() { + StringBuilder buf = new StringBuilder( columns.size() * 15 + tableName.length() + 10 ); + if ( comment != null ) { + buf.append( "/* " ).append( Dialect.escapeComment( comment ) ).append( " */ " ); + } + buf.append( "insert into " ).append( tableName ); + if ( columns.size() == 0 ) { + if ( getDialect().supportsNoColumnsInsert() ) { + // This line is missing in ORM + buf.append( " output inserted." ).append( identityColumnName ); + buf.append( ' ' ).append( getDialect().getNoColumnsInsertString() ); + } + else { + throw new MappingException( String.format( + "The INSERT statement for table [%s] contains no column, and this is not supported by [%s]", + tableName, + getDialect() + ) + ); + } + } + else { + buf.append( " (" ); + Iterator iter = columns.keySet().iterator(); + while ( iter.hasNext() ) { + buf.append( iter.next() ); + if ( iter.hasNext() ) { + buf.append( ", " ); + } + } + buf.append( ")"); + // This line is missing in ORM + buf.append( " output inserted." ).append( identityColumnName ); + buf.append( " values (" ); + iter = columns.values().iterator(); + while ( iter.hasNext() ) { + buf.append( iter.next() ); + if ( iter.hasNext() ) { + buf.append( ", " ); + } + } + buf.append( ')' ); + } + return buf.toString(); + } + } + +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java index 12229e825..ac8cf4971 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java @@ -5,6 +5,10 @@ */ package org.hibernate.reactive.persister.entity.impl; +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.util.concurrent.CompletionStage; + import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; @@ -15,6 +19,8 @@ import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.IdentifierGenerator; +import org.hibernate.id.IdentityGenerator; import org.hibernate.jdbc.Expectation; import org.hibernate.loader.entity.UniqueEntityLoader; import org.hibernate.mapping.PersistentClass; @@ -25,10 +31,6 @@ import org.hibernate.reactive.loader.entity.impl.ReactiveCascadeEntityLoader; import org.hibernate.type.Type; -import java.io.Serializable; -import java.sql.PreparedStatement; -import java.util.concurrent.CompletionStage; - /** * An {@link ReactiveEntityPersister} backed by {@link JoinedSubclassEntityPersister} * and {@link ReactiveAbstractEntityPersister}. @@ -115,6 +117,15 @@ public String generateIdentityInsertString(boolean[] includeProperty) { return parameters().process( sql, includeProperty.length ); } + @Override + public IdentifierGenerator getIdentifierGenerator() throws HibernateException { + final IdentifierGenerator identifierGenerator = super.getIdentifierGenerator(); + if ( identifierGenerator instanceof IdentityGenerator ) { + return new ReactiveIdentityGenerator(); + } + return identifierGenerator; + } + @Override public boolean hasProxy() { return hasUnenhancedProxy(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java index fe49e5c44..ff6037bf5 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java @@ -21,6 +21,8 @@ import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.IdentifierGenerator; +import org.hibernate.id.IdentityGenerator; import org.hibernate.jdbc.Expectation; import org.hibernate.loader.entity.UniqueEntityLoader; import org.hibernate.mapping.PersistentClass; @@ -127,6 +129,15 @@ public String generateIdentityInsertString(boolean[] includeProperty) { return parameters().process( sql, includeProperty.length ); } + @Override + public IdentifierGenerator getIdentifierGenerator() throws HibernateException { + final IdentifierGenerator identifierGenerator = super.getIdentifierGenerator(); + if ( identifierGenerator instanceof IdentityGenerator ) { + return new ReactiveIdentityGenerator(); + } + return identifierGenerator; + } + @Override public boolean hasProxy() { return hasUnenhancedProxy(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java index 0c147c99f..ed9304b96 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java @@ -5,6 +5,10 @@ */ package org.hibernate.reactive.persister.entity.impl; +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.util.concurrent.CompletionStage; + import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; @@ -15,6 +19,8 @@ import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.IdentifierGenerator; +import org.hibernate.id.IdentityGenerator; import org.hibernate.jdbc.Expectation; import org.hibernate.loader.entity.UniqueEntityLoader; import org.hibernate.mapping.PersistentClass; @@ -25,10 +31,6 @@ import org.hibernate.reactive.loader.entity.impl.ReactiveCascadeEntityLoader; import org.hibernate.type.Type; -import java.io.Serializable; -import java.sql.PreparedStatement; -import java.util.concurrent.CompletionStage; - /** * An {@link ReactiveEntityPersister} backed by {@link UnionSubclassEntityPersister} * and {@link ReactiveAbstractEntityPersister}. @@ -114,6 +116,15 @@ public String generateIdentityInsertString(boolean[] includeProperty) { return parameters().process( sql, includeProperty.length ); } + @Override + public IdentifierGenerator getIdentifierGenerator() throws HibernateException { + final IdentifierGenerator identifierGenerator = super.getIdentifierGenerator(); + if ( identifierGenerator instanceof IdentityGenerator ) { + return new ReactiveIdentityGenerator(); + } + return identifierGenerator; + } + @Override public boolean hasProxy() { return hasUnenhancedProxy(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/BatchingConnection.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/BatchingConnection.java index 5832676d6..28a8c016e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/BatchingConnection.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/BatchingConnection.java @@ -109,6 +109,10 @@ public CompletionStage execute(String sql) { return delegate.execute(sql); } + public CompletionStage executeUnprepared(String sql) { + return delegate.executeUnprepared( sql); + } + public CompletionStage executeOutsideTransaction(String sql) { return delegate.executeOutsideTransaction(sql); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnection.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnection.java index d30e1b162..0fbe72c97 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnection.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnection.java @@ -35,13 +35,16 @@ interface Expectation { } CompletionStage execute(String sql); - CompletionStage executeOutsideTransaction(String sql); + /** + * Run sql as statement (instead of preparedStatement) + */ + CompletionStage executeUnprepared(String sql); + CompletionStage update(String sql); CompletionStage update(String sql, Object[] paramValues); - CompletionStage update(String sql, Object[] paramValues, - boolean allowBatching, Expectation expectation); + CompletionStage update(String sql, Object[] paramValues, boolean allowBatching, Expectation expectation); CompletionStage update(String sql, List paramValues); CompletionStage select(String sql); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/DefaultSqlClientPool.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/DefaultSqlClientPool.java index 1140cf973..6aaee149e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/DefaultSqlClientPool.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/DefaultSqlClientPool.java @@ -7,9 +7,6 @@ import java.lang.invoke.MethodHandles; import java.net.URI; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.ServiceConfigurationError; import java.util.ServiceLoader; @@ -37,7 +34,6 @@ import io.vertx.sqlclient.SqlConnectOptions; import io.vertx.sqlclient.spi.Driver; -import static java.util.Arrays.asList; import static java.util.Collections.singletonList; /** @@ -64,15 +60,49 @@ public class DefaultSqlClientPool extends SqlClientPool private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); - /** - * The valid schemes for each driver - */ - private static final Map> DRIVER_SCHEMES = new HashMap<>(); + private enum VertxDriver { + DB2( "io.vertx.db2client.spi.DB2Driver", "db2" ), + MYSQL( "io.vertx.mysqlclient.spi.MySQLDriver", "mysql", "mariadb" ), + POSTGRES( "io.vertx.pgclient.spi.PgDriver", "postgres", "postgre", "postgresql", "cockroachdb" ), + MSSQL( "io.vertx.mssqlclient.spi.MSSQLDriver", "sqlserver" ); + + private final String className; + private final String[] schemas; - static { - DRIVER_SCHEMES.put( "io.vertx.db2client.spi.DB2Driver", asList( "db2" ) ); - DRIVER_SCHEMES.put( "io.vertx.mysqlclient.spi.MySQLDriver", asList( "mysql", "mariadb" ) ); - DRIVER_SCHEMES.put( "io.vertx.pgclient.spi.PgDriver", asList( "postgre", "postgres", "postgresql", "cockroachdb" ) ); + VertxDriver(String className, String... schemas) { + this.className = className; + this.schemas = schemas; + } + + /** + * Does the driver support the schema? + * + * @param schema the url schema + * @return true if the driver supports the schema, false otherwise + */ + public boolean matches(String schema) { + for ( String alias : schemas ) { + if ( alias.equalsIgnoreCase( schema ) ) { + return true; + } + } + return false; + } + + /** + * Find the Vert.x driver for the given class name. + * + * @param className the canonical name of the driver class + * @return a {@link VertxDriver} or null + */ + public static VertxDriver findByClassName(String className) { + for ( VertxDriver driver : values() ) { + if ( driver.className.equalsIgnoreCase( className ) ) { + return driver; + } + } + return null; + } } private Pool pools; @@ -185,22 +215,20 @@ protected URI jdbcUrl(Map configurationValues) { * @return the disambiguated {@link Driver} */ private Driver findDriver(URI uri, ServiceConfigurationError originalError) { - String scheme = uri.getScheme().toLowerCase( Locale.ROOT ); // "postgresql", "mysql", "db2", etc + String scheme = uri.getScheme(); // "postgresql", "mysql", "db2", etc for ( Driver d : ServiceLoader.load( Driver.class ) ) { String driverName = d.getClass().getCanonicalName(); LOG.detectedDriver( driverName ); - if ( driverFound( scheme, driverName ) ) { + if ( matchesScheme( driverName, scheme ) ) { return d; } } throw new ConfigurationException( "No suitable drivers found for URI scheme: " + uri.getScheme(), originalError ); } - private static boolean driverFound(String scheme, String driverName) { - if ( DRIVER_SCHEMES.containsKey( driverName ) ) { - return DRIVER_SCHEMES.get( driverName ).contains( scheme ); - } - return false; + private boolean matchesScheme(String driverName, String scheme) { + VertxDriver vertxDriver = VertxDriver.findByClassName( driverName ); + return vertxDriver != null && vertxDriver.matches( scheme ); } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/DefaultSqlClientPoolConfiguration.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/DefaultSqlClientPoolConfiguration.java index 062da45c0..addb0aed8 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/DefaultSqlClientPoolConfiguration.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/DefaultSqlClientPoolConfiguration.java @@ -90,16 +90,35 @@ public PoolOptions poolOptions() { @Override public SqlConnectOptions connectOptions(URI uri) { - String scheme = uri.getScheme(); + String path = uri.getPath(); + + String database = path.length() > 0 + ? path.substring( 1 ) + : ""; - String database = uri.getPath().substring( 1 ); if ( scheme.equals("db2") && database.indexOf( ':' ) > 0 ) { // DB2 URLs are a bit odd and have the format: // jdbc:db2://:/:key1=value1;key2=value2; database = database.substring( 0, database.indexOf( ':' ) ); } + String host = uri.getHost(); + int port = uri.getPort(); + int index = uri.toString().indexOf( ';' ); + if ( scheme.equals( "sqlserver" ) && index > 0 ) { + // SQL Server separates parameters in the url with a semicolon (';') + // and the URI class doesn't get the right value for host and port when the url + // contains parameters + URI uriWithoutParams = URI.create( uri.toString().substring( 0, index ) ); + host = uriWithoutParams.getHost(); + port = uriWithoutParams.getPort(); + } + + if ( port == -1 ) { + port = defaultPort( scheme ); + } + //see if the credentials were specified via properties String username = user; String password = pass; @@ -125,6 +144,17 @@ public SqlConnectOptions connectOptions(URI uri) { params = uri.getPath().substring(queryIndex).split(";"); } } + else if ( scheme.contains( "sqlserver" ) ) { + // SQL Server separates parameters in the url with a semicolon (';') + // Ex: jdbc:sqlserver://:;=AdventureWorks;user=;password= + String query = uri.getQuery(); + String rawQuery = uri.getRawQuery(); + String s = uri.toString(); + int queryIndex = s.indexOf(';') + 1; + if (queryIndex > 0) { + params = s.substring(queryIndex).split(";"); + } + } else { final String query = uri.getQuery(); if ( query != null ) { @@ -134,30 +164,27 @@ public SqlConnectOptions connectOptions(URI uri) { for (String param : params) { if ( param.startsWith("user=") ) { username = param.substring(5); - } - if ( param.startsWith("pass=") ) { + } else if ( param.startsWith("pass=") ) { password = param.substring(5); - } - if ( param.startsWith("password=") ) { + } else if ( param.startsWith("password=") ) { password = param.substring(9); + } else if ( param.startsWith("database=") ) { + database = param.substring(9); } } } } - int port = uri.getPort() == -1 - ? defaultPort( scheme ) - : uri.getPort(); - if ( username == null ) { throw new HibernateError( "database username not specified (set the property 'javax.persistence.jdbc.user', or include it as a parameter in the connection URL)" ); } SqlConnectOptions connectOptions = new SqlConnectOptions() - .setHost( uri.getHost() ) + .setHost( host ) .setPort( port ) .setDatabase( database ) .setUser( username ); + if (password != null) { connectOptions.setPassword( password ); } @@ -197,6 +224,8 @@ private int defaultPort(String scheme) { return 50000; case "cockroachdb": return 26257; + case "sqlserver": + return 1433; default: throw new IllegalArgumentException( "Unknown default port for scheme: " + scheme ); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/Parameters.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/Parameters.java index f05cca85b..74d9ca97e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/Parameters.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/Parameters.java @@ -8,18 +8,17 @@ import org.hibernate.dialect.CockroachDB192Dialect; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.PostgreSQL9Dialect; +import org.hibernate.dialect.SQLServerDialect; /** - * PostgreSQL has a "funny" parameter syntax of form {@code $n}, which + * Some databases have a different parameter syntax, which * the Vert.x {@link io.vertx.sqlclient.SqlClient} does not abstract. * This class converts JDBC/ODBC-style {@code ?} parameters generated - * by Hibernate ORM to this native format. + * by Hibernate ORM to the native format. */ -public class Parameters { +public abstract class Parameters { - private static Parameters INSTANCE = new Parameters(); - - private static Parameters NO_PARSING = new Parameters() { + private static final Parameters NO_PARSING = new Parameters() { @Override public String process(String sql) { return sql; @@ -37,124 +36,20 @@ public String processLimit(String sql, Object[] parameterArray, boolean hasOffse }; public static Parameters instance(Dialect dialect) { - return dialect instanceof PostgreSQL9Dialect || dialect instanceof CockroachDB192Dialect - ? INSTANCE - : NO_PARSING; - } - - private Parameters() { - } - - public String process(String sql) { - if ( isProcessingNotRequired( sql ) ) { - return sql; - } - return new Parser( sql ).result(); - } - - /** - * Limit and offset gets applied just before the execution of the query but because we know - * how the string looks like for Postgres, it's faster to replace the last bit instead - * of processing the whole query - */ - public String processLimit(String sql, Object[] parameterArray, boolean hasOffset) { - if ( isProcessingNotRequired( sql ) ) { - return sql; - } - - // Replace 'limit ? offset ?' with the $ style parameters for PostgreSQL - int index = hasOffset ? parameterArray.length - 1 : parameterArray.length; - int pos = sql.indexOf( " limit ?" ); - if ( pos > -1 ) { - String sqlProcessed = sql.substring( 0, pos ) + " limit $" + index++; - if ( hasOffset ) { - sqlProcessed += " offset $" + index; - } - return sqlProcessed; - } - - return sql; + if (dialect instanceof PostgreSQL9Dialect || dialect instanceof CockroachDB192Dialect) return PostgresParameters.INSTANCE; + if (dialect instanceof SQLServerDialect) return SQLServerParameters.INSTANCE; + return NO_PARSING; } - /** - * Replace all JDBC-style {@code ?} parameters with Postgres-style - * {@code $n} parameters in the given SQL string. - */ - public String process(String sql, int parameterCount) { - if ( isProcessingNotRequired( sql ) ) { - return sql; - } - return new Parser( sql, parameterCount ).result(); - } - - private static boolean isProcessingNotRequired(String sql) { + public static boolean isProcessingNotRequired(String sql) { return sql == null // There aren't any parameters - || sql.indexOf( '?' ) == -1; + || sql.indexOf('?') == -1; } - private static class Parser { - - private boolean inString; - private boolean inQuoted; - private boolean inSqlComment; - private boolean inCComment; - private boolean escaped; - private int count = 0; - private StringBuilder result; - private int previous; + public abstract String process(String sql); - private Parser(String sql) { - this( sql, 10 ); - } + public abstract String process(String sql, int parameterCount); - private Parser(String sql, int parameterCount) { - result = new StringBuilder( sql.length() + parameterCount ); - sql.codePoints().forEach( this::append ); - } - - private String result() { - return result.toString(); - } - - private void append(int codePoint) { - if ( escaped ) { - escaped = false; - } - else { - switch ( codePoint ) { - case '\\': - escaped = true; - break; - case '"': - if ( !inString && !inSqlComment && !inCComment ) inQuoted = !inQuoted; - break; - case '\'': - if ( !inQuoted && !inSqlComment && !inCComment ) inString = !inString; - break; - case '-': - if ( !inQuoted && !inString && !inCComment && previous == '-' ) inSqlComment = true; - break; - case '\n': - inSqlComment = false; - break; - case '*': - if ( !inQuoted && !inString && !inSqlComment && previous == '/' ) inCComment = true; - break; - case '/': - if ( previous == '*' ) inCComment = false; - break; - //TODO: $$-quoted strings - case '?': - if ( !inQuoted && !inString ) { - result.append( '$' ).append( ++count ); - previous = '?'; - return; - } - } - } - previous = codePoint; - result.appendCodePoint( codePoint ); - } - } + public abstract String processLimit(String sql, Object[] parameterArray, boolean hasOffset); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/PostgresParameters.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/PostgresParameters.java new file mode 100644 index 000000000..2e0e50d81 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/PostgresParameters.java @@ -0,0 +1,120 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.pool.impl; + +public class PostgresParameters extends Parameters { + + public static final PostgresParameters INSTANCE = new PostgresParameters(); + + private PostgresParameters() { + } + + public String process(String sql) { + if (isProcessingNotRequired(sql)) { + return sql; + } + return new Parser(sql).result(); + } + + /** + * Limit and offset gets applied just before the execution of the query but because we know + * how the string looks like for Postgres, it's faster to replace the last bit instead + * of processing the whole query + */ + public String processLimit(String sql, Object[] parameterArray, boolean hasOffset) { + if (isProcessingNotRequired(sql)) { + return sql; + } + + // Replace 'limit ? offset ?' with the $ style parameters for PostgreSQL + int index = hasOffset ? parameterArray.length - 1 : parameterArray.length; + int pos = sql.indexOf(" limit ?"); + if (pos > -1) { + String sqlProcessed = sql.substring(0, pos) + " limit $" + index++; + if (hasOffset) { + sqlProcessed += " offset $" + index; + } + return sqlProcessed; + } + + return sql; + } + + /** + * Replace all JDBC-style {@code ?} parameters with Postgres-style + * {@code $n} parameters in the given SQL string. + */ + public String process(String sql, int parameterCount) { + if (isProcessingNotRequired(sql)) { + return sql; + } + return new Parser(sql, parameterCount).result(); + } + + private static class Parser { + + private boolean inString; + private boolean inQuoted; + private boolean inSqlComment; + private boolean inCComment; + private boolean escaped; + private int count = 0; + private StringBuilder result; + private int previous; + + private Parser(String sql) { + this(sql, 10); + } + + private Parser(String sql, int parameterCount) { + result = new StringBuilder(sql.length() + parameterCount); + sql.codePoints().forEach(this::append); + } + + private String result() { + return result.toString(); + } + + private void append(int codePoint) { + if (escaped) { + escaped = false; + } else { + switch (codePoint) { + case '\\': + escaped = true; + break; + case '"': + if (!inString && !inSqlComment && !inCComment) inQuoted = !inQuoted; + break; + case '\'': + if (!inQuoted && !inSqlComment && !inCComment) inString = !inString; + break; + case '-': + if (!inQuoted && !inString && !inCComment && previous == '-') inSqlComment = true; + break; + case '\n': + inSqlComment = false; + break; + case '*': + if (!inQuoted && !inString && !inSqlComment && previous == '/') inCComment = true; + break; + case '/': + if (previous == '*') inCComment = false; + break; + //TODO: $$-quoted strings + case '?': + if (!inQuoted && !inString) { + result.append('$').append(++count); + previous = '?'; + return; + } + } + } + previous = codePoint; + result.appendCodePoint(codePoint); + } + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/ProxyConnection.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/ProxyConnection.java index 3471021f9..608b1ee7a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/ProxyConnection.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/ProxyConnection.java @@ -75,6 +75,11 @@ public CompletionStage execute(String sql) { return withConnection( conn -> conn.execute( sql ) ); } + @Override + public CompletionStage executeUnprepared(String sql) { + return withConnection( conn -> conn.executeUnprepared( sql ) ); + } + @Override public CompletionStage executeOutsideTransaction(String sql) { return withConnection( conn -> conn.executeOutsideTransaction( sql ) ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SQLServerParameters.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SQLServerParameters.java new file mode 100644 index 000000000..56c3e3a0f --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SQLServerParameters.java @@ -0,0 +1,118 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.pool.impl; + +public class SQLServerParameters extends Parameters { + + public static final SQLServerParameters INSTANCE = new SQLServerParameters(); + + private SQLServerParameters() { + } + + @Override + public String process(String sql) { + if (isProcessingNotRequired(sql)) { + return sql; + } + return new Parser(sql).result(); + } + + @Override + public String process(String sql, int parameterCount) { + if (isProcessingNotRequired(sql)) { + return sql; + } + return new Parser(sql, parameterCount).result(); + } + + /* Offset and Fetch gets applied just before the execution of the query but because we know + * how the string looks like for Sql Server, it's faster to replace the last bit instead + * of processing the whole query + */ + @Override + public String processLimit(String sql, Object[] parameterArray, boolean hasOffset) { + if (isProcessingNotRequired(sql)) { + return sql; + } + + // Replace 'offset ? fetch next ? rows only' with the @P style parameters for Sql Server + int index = hasOffset ? parameterArray.length - 1 : parameterArray.length; + int pos = sql.indexOf( " offset ?" ); + if ( pos > -1 ) { + String sqlProcessed = sql.substring( 0, pos ) + " offset @P" + index++ + " rows"; + if ( sql.indexOf( " fetch next ?" ) > -1 ) { + sqlProcessed += " fetch next @P" + index + " rows only "; + } + return sqlProcessed; + } + + return sql; + } + + private static class Parser { + + private boolean inString; + private boolean inQuoted; + private boolean inSqlComment; + private boolean inCComment; + private boolean escaped; + private int count = 0; + private StringBuilder result; + private int previous; + + private Parser(String sql) { + this(sql, 10); + } + + private Parser(String sql, int parameterCount) { + result = new StringBuilder(sql.length() + parameterCount); + sql.codePoints().forEach(this::append); + } + + private String result() { + return result.toString(); + } + + private void append(int codePoint) { + if (escaped) { + escaped = false; + } else { + switch (codePoint) { + case '\\': + escaped = true; + break; + case '"': + if (!inString && !inSqlComment && !inCComment) inQuoted = !inQuoted; + break; + case '\'': + if (!inQuoted && !inSqlComment && !inCComment) inString = !inString; + break; + case '-': + if (!inQuoted && !inString && !inCComment && previous == '-') inSqlComment = true; + break; + case '\n': + inSqlComment = false; + break; + case '*': + if (!inQuoted && !inString && !inSqlComment && previous == '/') inCComment = true; + break; + case '/': + if (previous == '*') inCComment = false; + break; + //TODO: $$-quoted strings + case '?': + if (!inQuoted && !inString) { + result.append("@P").append(++count); + previous = '?'; + return; + } + } + } + previous = codePoint; + result.appendCodePoint(codePoint); + } + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java index eac9e5e8e..a07b7170d 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java @@ -21,6 +21,7 @@ import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.pool.ReactiveConnection; +import org.hibernate.reactive.util.impl.CompletionStages; import io.vertx.sqlclient.Pool; import io.vertx.sqlclient.PropertyKind; @@ -120,6 +121,13 @@ public CompletionStage execute(String sql) { return preparedQuery( sql ).thenApply( ignore -> null ); } + @Override + public CompletionStage executeUnprepared(String sql) { + feedback( sql ); + return client().query( sql ).execute().toCompletionStage() + .thenCompose( CompletionStages::voidFuture ); + } + @Override public CompletionStage executeOutsideTransaction(String sql) { return preparedQueryOutsideTransaction( sql ).thenApply( ignore -> null ); @@ -311,6 +319,7 @@ private static NullValue toNullValue(JDBCType jdbcType) { return NullValue.Short; case Types.DECIMAL: return NullValue.BigDecimal; + case Types.VARBINARY: case Types.BINARY: case Types.BLOB: case Types.LONGVARBINARY: 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 682b30e67..ad4f0919c 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 @@ -12,6 +12,7 @@ import org.hibernate.dialect.MariaDB103Dialect; import org.hibernate.dialect.MySQL8Dialect; import org.hibernate.dialect.PostgreSQL10Dialect; +import org.hibernate.dialect.SQLServer2012Dialect; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; import org.hibernate.engine.jdbc.dialect.spi.DatabaseMetaDataDialectResolutionInfoAdapter; @@ -135,6 +136,9 @@ else if ( url.startsWith("db2:") ) { else if ( url.startsWith("cockroachdb:") ) { return CockroachDB201Dialect.class; } + else if ( url.startsWith("sqlserver:") ) { + return SQLServer2012Dialect.class; + } else { return null; } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveQueryTranslatorImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveQueryTranslatorImpl.java index 895cfd55c..8e906551f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveQueryTranslatorImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveQueryTranslatorImpl.java @@ -30,19 +30,17 @@ import org.hibernate.internal.util.collections.IdentitySet; import org.hibernate.loader.hql.QueryLoader; import org.hibernate.param.ParameterSpecification; -import org.hibernate.reactive.adaptor.impl.QueryParametersAdaptor; import org.hibernate.reactive.bulk.StatementsWithParameters; import org.hibernate.reactive.loader.hql.impl.ReactiveQueryLoader; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; -import org.hibernate.reactive.pool.ReactiveConnection; import org.hibernate.reactive.pool.impl.Parameters; import org.hibernate.reactive.session.ReactiveQueryExecutor; -import org.hibernate.reactive.util.impl.CompletionStages; import antlr.RecognitionException; import antlr.collections.AST; + /** * A {@link org.hibernate.hql.spi.FilterTranslator} that adds reactive versions of the * existing methods in the interface and throw an exception if the non-reactive variant @@ -168,50 +166,10 @@ public CompletionStage> reactiveList(SharedSessionContractImplementor se * The reactive version of * {@link QueryTranslatorImpl#executeUpdate(QueryParameters, SharedSessionContractImplementor)}. */ - public CompletionStage executeReactiveUpdate(QueryParameters queryParameters, - ReactiveQueryExecutor session) { + public CompletionStage executeReactiveUpdate(QueryParameters queryParameters, ReactiveQueryExecutor session) { errorIfSelect(); - StatementsWithParameters statementsWithParameters = getUpdateHandler(); - String[] statements = statementsWithParameters.getSqlStatements(); - ParameterSpecification[][] specifications = statementsWithParameters.getParameterSpecifications(); - - return CompletionStages.total( - 0, statements.length, - i -> executeStatement( - statements[i], - QueryParametersAdaptor.arguments( - queryParameters, - specifications[i], - session.getSharedContract() - ), - statementsWithParameters, - session - ) - ); - } - - private CompletionStage executeStatement(String sql, - Object[] arguments, - StatementsWithParameters statementsWithParameters, - ReactiveQueryExecutor session) { - ReactiveConnection connection = session.getReactiveConnection(); - if ( !statementsWithParameters.isSchemaDefinitionStatement( sql ) ) { - return connection.update( sql, arguments ); - } - else if ( statementsWithParameters.isTransactionalStatement( sql ) ) { - // a DML statement that should be executed within the - // transaction (local temporary tables) - return connection.execute( sql ).thenApply( v -> 0 ); - } - else { - // a DML statement that should be executed outside the - // transaction (global temporary tables) - return connection.executeOutsideTransaction( sql ) - // ignore errors creating tables, since a create - // table fails whenever the table already exists - .handle( (v, x) -> 0 ); - } + return getUpdateHandler().execute( session, queryParameters ); } private String[] process(String[] sqlStatements, int paramLength) { @@ -232,8 +190,7 @@ else if (executor instanceof MultiTableDeleteExecutor) { } else { return new StatementsWithParameters() { - final ParameterSpecification[] parameterSpecifications = - getCollectedParameterSpecifications().toArray( new ParameterSpecification[0] ); + final ParameterSpecification[] parameterSpecifications = getCollectedParameterSpecifications().toArray( new ParameterSpecification[0] ); final String[] statements = process( executor.getSqlStatements(), parameterSpecifications.length ); @Override @@ -247,11 +204,6 @@ public ParameterSpecification[][] getParameterSpecifications() { Arrays.fill( result, parameterSpecifications ); return result; } - - @Override - public boolean isSchemaDefinitionStatement(String statement) { - return false; - } }; } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/util/impl/CompletionStages.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/util/impl/CompletionStages.java index 23c743e9f..eae1bc684 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/util/impl/CompletionStages.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/util/impl/CompletionStages.java @@ -64,6 +64,10 @@ public static CompletionStage zeroFuture() { return ZERO; } + public static CompletionStage zeroFuture(Object ignore) { + return zeroFuture(); + } + public static CompletionStage trueFuture() { return TRUE; } @@ -105,6 +109,13 @@ public static Ret returnOrRethrow(Throwable x, Ret re return result; } + /** + * For CompletionStage#handle when we don't care about errors + */ + public static U ignoreErrors(Void unused, Throwable throwable) { + return null; + } + public static void logSqlException(Throwable t, Supplier message, String sql) { if ( t != null ) { LOG.failedToExecuteStatement( sql, message.get(), t ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchQueryOnConnectionTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchQueryOnConnectionTest.java index 040f6771e..1b1793d11 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchQueryOnConnectionTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchQueryOnConnectionTest.java @@ -6,6 +6,7 @@ package org.hibernate.reactive; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletionStage; @@ -14,6 +15,8 @@ import org.hibernate.cfg.Configuration; import org.hibernate.reactive.pool.ReactiveConnection; +import org.hibernate.reactive.pool.impl.PostgresParameters; +import org.hibernate.reactive.pool.impl.SQLServerParameters; import org.junit.After; import org.junit.Test; @@ -23,6 +26,7 @@ import static org.hibernate.reactive.containers.DatabaseConfiguration.dbType; public class BatchQueryOnConnectionTest extends BaseReactiveTest { + private static final int BATCH_SIZE = 20; @After @@ -60,10 +64,7 @@ public void testBatchInsertUpdateSizeGtMultiple(TestContext context) { } public List> doBatchInserts(TestContext context, int nEntities, int nEntitiesMultiple) { - final String insertSql = "insert into DataPoint (description, x, y, id) values "; - final String sql = dbType().requiresDollarSyntax() - ? insertSql + "($1, $2, $3, $4)" - : insertSql + "(?, ?, ?, ?)"; + final String insertSql = process( "insert into DataPoint (description, x, y, id) values (?, ?, ?, ?)" ); List> paramsBatches = new ArrayList<>(); List paramsBatch = new ArrayList<>( BATCH_SIZE ); @@ -72,8 +73,8 @@ public List> doBatchInserts(TestContext context, int nEntities, i DataPoint dp = new DataPoint(i); dp.description = "#" + i; - dp.setX(new BigDecimal(i * 0.1d).setScale(19, BigDecimal.ROUND_DOWN)); - dp.setY(new BigDecimal(dp.getX().doubleValue()*Math.PI).setScale(19, BigDecimal.ROUND_DOWN)); + dp.setX( new BigDecimal( i * 0.1d ).setScale( 19, RoundingMode.DOWN ) ); + dp.setY( new BigDecimal( dp.getX().doubleValue() * Math.PI ).setScale( 19, RoundingMode.DOWN ) ); //uncomment to expose bug in DB2 client: // dp.setY(new BigDecimal(Math.cos(dp.getX().doubleValue())).setScale(19, BigDecimal.ROUND_DOWN)); @@ -91,7 +92,7 @@ public List> doBatchInserts(TestContext context, int nEntities, i CompletionStage stage = connection(); for ( List batch : paramsBatches ) { - stage = stage.thenCompose( connection -> connection.update( sql, batch ) + stage = stage.thenCompose( connection -> connection.update( insertSql, batch ) .thenApply( updateCounts -> { context.assertEquals( batch.size(), updateCounts.length ); for ( int updateCount : updateCounts ) { @@ -116,6 +117,18 @@ public List> doBatchInserts(TestContext context, int nEntities, i return paramsBatches; } + private String process(String sql) { + switch ( dbType() ) { + case POSTGRESQL: + case COCKROACHDB: + return PostgresParameters.INSTANCE.process( sql ); + case SQLSERVER: + return SQLServerParameters.INSTANCE.process( sql ); + default: + return sql; + } + } + protected Configuration constructConfiguration() { Configuration configuration = super.constructConfiguration(); configuration.addAnnotatedClass( DataPoint.class ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/IdentityGeneratorWithColumnTransformerTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/IdentityGeneratorWithColumnTransformerTest.java new file mode 100644 index 000000000..3d306e283 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/IdentityGeneratorWithColumnTransformerTest.java @@ -0,0 +1,132 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletionStage; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.hibernate.annotations.ColumnTransformer; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; + +import org.junit.Test; + +import io.vertx.ext.unit.TestContext; + +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; +import static org.hibernate.reactive.containers.DatabaseConfiguration.dbType; + +/** + * Similar to {@link IdentityGeneratorTest} but enables SQL comments and uses a {@link ColumnTransformer}. + */ +public class IdentityGeneratorWithColumnTransformerTest extends BaseReactiveTest { + + // Comments don't work with Db2 + private static final String ENABLE_COMMENTS = String.valueOf( dbType() != DB2 ); + + /** + * When {@link AvailableSettings#USE_GET_GENERATED_KEYS} is enabled, different + * queries will be used for each datastore to get the id + */ + public static class EnableUseGetGeneratedKeys extends IdentityGeneratorWithColumnTransformerTest { + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + configuration.setProperty( AvailableSettings.USE_GET_GENERATED_KEYS, "true" ); + return configuration; + } + } + + // The number of entities we want to create + private static final int ENTITY_NUMBER = 100; + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + configuration.addAnnotatedClass( EntityWithIdentity.class ); + configuration.setProperty( AvailableSettings.USE_SQL_COMMENTS, ENABLE_COMMENTS ); + return configuration; + } + + private CompletionStage populateDb(TestContext context) { + final List identities = new ArrayList<>( ENTITY_NUMBER ); + for ( int i = 0; i < ENTITY_NUMBER; i++ ) { + identities.add( new EntityWithIdentity( i ) ); + } + return getSessionFactory() + .withTransaction( (session, tx) -> session.persist( identities.toArray() ) ) + .thenAccept( ignore -> { + Long assignedId = 0L; + for ( EntityWithIdentity identity : identities ) { + context.assertNotNull( identity.id ); + context.assertTrue( identity.id > assignedId ); + assignedId = identity.id; + } + } ); + } + + @Test + public void testIdentityGenerator(TestContext context) { + test( context, populateDb( context ) + .thenCompose( v -> openSession() ) + .thenCompose( session -> + session.createQuery( "FROM EntityWithIdentity ORDER BY position ASC", EntityWithIdentity.class ) + .getResultList() ) + .thenAccept( list -> { + context.assertEquals( ENTITY_NUMBER, list.size() ); + int i = 0; + for ( EntityWithIdentity entity : list ) { + context.assertEquals( i * 2, entity.getPosition() ); + i++; + } + } ) ); + } + + @Entity(name = "EntityWithIdentity") + @Table( name = "`Table with default values`") + private static class EntityWithIdentity { + private static final String PREFIX = "Entity: "; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(unique = true) + String name; + + @Column(name = "position") + @ColumnTransformer(forColumn = "position", write = "? * 2") + private int position; + + public EntityWithIdentity() { + } + + public EntityWithIdentity(int index) { + this.name = PREFIX + index; + this.position = index; + } + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } + + @Override + public String toString() { + return id + ":" + name + ":" + position; + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UriConfigTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UriConfigTest.java index ce7102968..97f6c38b8 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UriConfigTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UriConfigTest.java @@ -13,6 +13,7 @@ import org.hibernate.dialect.MariaDB103Dialect; import org.hibernate.dialect.MySQL8Dialect; import org.hibernate.dialect.PostgreSQL10Dialect; +import org.hibernate.dialect.SQLServer2012Dialect; import org.hibernate.reactive.containers.DatabaseConfiguration; import org.hibernate.reactive.provider.Settings; @@ -32,6 +33,7 @@ protected Configuration constructConfiguration() { case COCKROACHDB: dialect = CockroachDB201Dialect.class; break; case MYSQL: dialect = MySQL8Dialect.class; break; case MARIA: dialect = MariaDB103Dialect.class; break; + case SQLSERVER: dialect = SQLServer2012Dialect.class; break; case DB2: dialect = DB297Dialect.class; break; default: throw new IllegalArgumentException("Database not recognized: " + dbType().name()); } @@ -55,6 +57,7 @@ private String selectQuery() { switch ( dbType() ) { case POSTGRESQL: case COCKROACHDB: + case SQLSERVER: return "select cast(current_timestamp as varchar)"; case MARIA: case MYSQL: diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UriPoolConfiguration.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UriPoolConfiguration.java index 401662ad6..8a18cf7c1 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UriPoolConfiguration.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UriPoolConfiguration.java @@ -6,6 +6,7 @@ package org.hibernate.reactive; import io.vertx.db2client.DB2ConnectOptions; +import io.vertx.mssqlclient.MSSQLConnectOptions; import io.vertx.mysqlclient.MySQLConnectOptions; import io.vertx.pgclient.PgConnectOptions; import io.vertx.sqlclient.PoolOptions; @@ -14,6 +15,8 @@ import java.net.URI; +import static org.hibernate.reactive.containers.DatabaseConfiguration.dbType; + public class UriPoolConfiguration implements SqlClientPoolConfiguration { @Override public PoolOptions poolOptions() { @@ -32,8 +35,9 @@ public SqlConnectOptions connectOptions(URI uri) { return MySQLConnectOptions.fromUri( uri.toString() ); case "db2": return DB2ConnectOptions.fromUri( uri.toString() ); - default: - throw new IllegalArgumentException(); + case "sqlserver": + return MSSQLConnectOptions.fromUri( uri.toString() ); + default: throw new IllegalArgumentException( "Database not recognized: " + dbType() ); } } } 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 6d8b93c69..fdb370c75 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 @@ -18,11 +18,12 @@ public class DatabaseConfiguration { public static final boolean USE_DOCKER = Boolean.getBoolean("docker"); public enum DBType { - DB2( DB2Database.INSTANCE, 50000, false ), - MYSQL( MySQLDatabase.INSTANCE, 3306, false ), - MARIA( MariaDatabase.INSTANCE, 3306, false, "mariadb" ), - POSTGRESQL( PostgreSQLDatabase.INSTANCE, 5432, true, "POSTGRES", "PG" ), - COCKROACHDB( CockroachDBDatabase.INSTANCE, 26257, true, "COCKROACH" ); + DB2( DB2Database.INSTANCE, 50000 ), + MYSQL( MySQLDatabase.INSTANCE, 3306 ), + MARIA( MariaDatabase.INSTANCE, 3306, "mariadb" ), + POSTGRESQL( PostgreSQLDatabase.INSTANCE, 5432, "POSTGRES", "PG" ), + COCKROACHDB( CockroachDBDatabase.INSTANCE, 26257, "COCKROACH" ), + SQLSERVER( MSSQLServerDatabase.INSTANCE, 1433, "MSSQL", "MSSQLSERVER" ); private final TestableDatabase configuration; private final int defaultPort; @@ -30,14 +31,10 @@ public enum DBType { // A list of alternative names that can be used to select the db private final String[] aliases; - // True if the database requires '$' style parameters for SQL queries - private boolean requiresDollarSyntax; - - DBType(TestableDatabase configuration, int defaultPort, boolean requiresDollarSyntax, String... aliases) { + DBType(TestableDatabase configuration, int defaultPort, String... aliases) { this.configuration = configuration; this.defaultPort = defaultPort; this.aliases = aliases; - this.requiresDollarSyntax = requiresDollarSyntax; } @Override @@ -69,10 +66,6 @@ public static DBType fromString(String dbName) { public int getDefaultPort() { return defaultPort; } - - public boolean requiresDollarSyntax() { - return requiresDollarSyntax; - } } public static final String USERNAME = "hreact"; diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java new file mode 100644 index 000000000..901426150 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java @@ -0,0 +1,73 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.containers; + +import org.testcontainers.containers.MSSQLServerContainer; + +/** + * The JDBC driver syntax is: + * jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]] + * + * But the Vert.x SQL client syntax is: + * sqlserver://[user[:[password]]@]host[:port][/database][?attribute1=value1&attribute2=value2…​] + */ +class MSSQLServerDatabase implements TestableDatabase { + + public static final String IMAGE_NAME = "mcr.microsoft.com/mssql/server:2019-latest"; + + public static final MSSQLServerDatabase INSTANCE = new MSSQLServerDatabase(); + + public static final String PASSWORD = "~!HReact!~"; + + /** + * Holds configuration for the Microsoft SQL Server database container. If the build is run with -Pdocker then + * Testcontainers+Docker will be used. + *

+ * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located + * at `$HOME/.testcontainers.properties` (create the file if it does not exist). + */ + public static final MSSQLServerContainer mssqlserver = new MSSQLServerContainer<>( IMAGE_NAME ) + .acceptLicense() + .withPassword( PASSWORD ) + .withReuse( true ); + + @Override + public String getJdbcUrl() { + return buildJdbcUrlWithCredentials( address() ); + } + + private String getRegularJdbcUrl() { + return "jdbc:sqlserver://localhost:1433"; + } + + @Override + public String getUri() { + return buildUriWithCredentials( address() ); + } + + private String address() { + if ( DatabaseConfiguration.USE_DOCKER ) { + // Calling start() will start the container (if not already started) + // It is required to call start() before obtaining the JDBC URL because it will contain a randomized port + mssqlserver.start(); + return mssqlserver.getJdbcUrl(); + } + + return getRegularJdbcUrl(); + } + + private String buildJdbcUrlWithCredentials(String jdbcUrl) { + return jdbcUrl + ";user=" + mssqlserver.getUsername() + ";password=" + mssqlserver.getPassword(); + } + + private static String buildUriWithCredentials(String jdbcUrl) { + return "sqlserver://" + mssqlserver.getUsername() + ":" + mssqlserver.getPassword() + "@" + jdbcUrl.substring( "jdbc:sqlserver://".length() ); + } + + private MSSQLServerDatabase() { + } + +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/BasicTypesForSelectedDBTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/BasicTypesForSelectedDBTest.java index c9fc2ebd7..ca6600c67 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/BasicTypesForSelectedDBTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/BasicTypesForSelectedDBTest.java @@ -5,21 +5,30 @@ */ package org.hibernate.reactive.types; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.unit.TestContext; +import java.util.Objects; +import java.util.function.Consumer; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; +import javax.persistence.Version; + import org.hibernate.annotations.Type; import org.hibernate.cfg.Configuration; import org.hibernate.reactive.BaseReactiveTest; import org.hibernate.reactive.testing.DatabaseSelectionRule; + import org.junit.After; import org.junit.Rule; import org.junit.Test; -import javax.persistence.*; -import java.util.Objects; -import java.util.function.Consumer; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.unit.TestContext; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.SQLSERVER; /** * Test types that we expect to work only on selected DBs. @@ -27,7 +36,7 @@ public class BasicTypesForSelectedDBTest extends BaseReactiveTest { @Rule - public DatabaseSelectionRule selectionRule = DatabaseSelectionRule.skipTestsFor( DB2 ); + public DatabaseSelectionRule selectionRule = DatabaseSelectionRule.skipTestsFor( DB2, SQLSERVER ); @Override protected Configuration constructConfiguration() { diff --git a/podman.md b/podman.md index b9e753869..44e5d417e 100644 --- a/podman.md +++ b/podman.md @@ -172,3 +172,28 @@ Optionally, you can connect to the database with the [Db2 command line interface podman exec -ti HibernateTestingDB2 bash -c "su - hreact -c db2 connect" ``` [db2-cli]:https://www.ibm.com/support/knowledgecenter/en/SSEPEK_11.0.0/comref/src/tpc/db2z_commandlineprocessor.html + +## Microsoft SQL Server + +Use the following command to start a [Microsoft SQL Server][mssql] database with the required credentials +and schema to run the tests: + +[mssql]:https://www.microsoft.com/en-gb/sql-server/ + +``` +podman run --rm -it --name HibernateTestingMSSQL -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD='~!HReact!~' -p 1433:1433 mcr.microsoft.com/mssql/server:2019-latest +``` + +When the database has started, you can run the tests on MS SQL Server with: + +``` +./gradlew test -Pdb=SqlServer +``` + +Optionally, you can connect to the database with the [sqlcmd utility][sqlcmd-cli] using: + +``` +podman exec -it HibernateTestingMSSQL /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P ~!HReact!~ +``` + +[sqlcmd-cli]:https://docs.microsoft.com/en-us/sql/tools/sqlcmd-utility?view=sql-server-ver15 diff --git a/tooling/jbang/CockroachDBReactiveTest.java.qute b/tooling/jbang/CockroachDBReactiveTest.java.qute index 598da0d5f..bbf14e704 100755 --- a/tooling/jbang/CockroachDBReactiveTest.java.qute +++ b/tooling/jbang/CockroachDBReactiveTest.java.qute @@ -5,8 +5,8 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-pg-client:$\{vertx.version:4.1.1} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.1.1} +//DEPS io.vertx:vertx-pg-client:$\{vertx.version:4.1.2} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.1.2} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:1.0.0.CR6} //DEPS org.assertj:assertj-core:3.19.0 //DEPS junit:junit:4.13.2 diff --git a/tooling/jbang/Db2ReactiveTest.java.qute b/tooling/jbang/Db2ReactiveTest.java.qute index a1da078ac..291bff809 100755 --- a/tooling/jbang/Db2ReactiveTest.java.qute +++ b/tooling/jbang/Db2ReactiveTest.java.qute @@ -5,8 +5,8 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-db2-client:$\{vertx.version:4.1.1} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.1.1} +//DEPS io.vertx:vertx-db2-client:$\{vertx.version:4.1.2} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.1.2} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:1.0.0.CR6} //DEPS org.assertj:assertj-core:3.19.0 //DEPS junit:junit:4.13.2 diff --git a/tooling/jbang/Example.java b/tooling/jbang/Example.java index e180cd8ce..074494b0a 100644 --- a/tooling/jbang/Example.java +++ b/tooling/jbang/Example.java @@ -5,9 +5,9 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-pg-client:${vertx.version:4.1.1} -//DEPS io.vertx:vertx-mysql-client:${vertx.version:4.1.1} -//DEPS io.vertx:vertx-db2-client:${vertx.version:4.1.1} +//DEPS io.vertx:vertx-pg-client:${vertx.version:4.1.2} +//DEPS io.vertx:vertx-mysql-client:${vertx.version:4.1.2} +//DEPS io.vertx:vertx-db2-client:${vertx.version:4.1.2} //DEPS org.hibernate.reactive:hibernate-reactive-core:${hibernate-reactive.version:1.0.0.CR6} //DEPS org.slf4j:slf4j-simple:1.7.30 diff --git a/tooling/jbang/MariaDBReactiveTest.java.qute b/tooling/jbang/MariaDBReactiveTest.java.qute index e6a518cf0..da72d29ee 100755 --- a/tooling/jbang/MariaDBReactiveTest.java.qute +++ b/tooling/jbang/MariaDBReactiveTest.java.qute @@ -5,8 +5,8 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-mysql-client:$\{vertx.version:4.1.1} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.1.1} +//DEPS io.vertx:vertx-mysql-client:$\{vertx.version:4.1.2} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.1.2} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:1.0.0.CR6} //DEPS org.assertj:assertj-core:3.19.0 //DEPS junit:junit:4.13.2 diff --git a/tooling/jbang/MySQLReactiveTest.java.qute b/tooling/jbang/MySQLReactiveTest.java.qute index 10d585277..756fa8a90 100755 --- a/tooling/jbang/MySQLReactiveTest.java.qute +++ b/tooling/jbang/MySQLReactiveTest.java.qute @@ -5,8 +5,8 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-mysql-client:$\{vertx.version:4.1.1} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.1.1} +//DEPS io.vertx:vertx-mysql-client:$\{vertx.version:4.1.2} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.1.2} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:1.0.0.CR6} //DEPS org.assertj:assertj-core:3.19.0 //DEPS junit:junit:4.13.2 diff --git a/tooling/jbang/PostgreSQLReactiveTest.java.qute b/tooling/jbang/PostgreSQLReactiveTest.java.qute index f0076e0f5..f24f5fb92 100755 --- a/tooling/jbang/PostgreSQLReactiveTest.java.qute +++ b/tooling/jbang/PostgreSQLReactiveTest.java.qute @@ -5,8 +5,8 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-pg-client:$\{vertx.version:4.1.1} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.1.1} +//DEPS io.vertx:vertx-pg-client:$\{vertx.version:4.1.2} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.1.2} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:1.0.0.CR6} //DEPS org.assertj:assertj-core:3.19.0 //DEPS junit:junit:4.13.2 diff --git a/tooling/jbang/ReactiveTest.java b/tooling/jbang/ReactiveTest.java index bc2987aea..f5e7eb4f5 100755 --- a/tooling/jbang/ReactiveTest.java +++ b/tooling/jbang/ReactiveTest.java @@ -5,10 +5,10 @@ */ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS io.vertx:vertx-pg-client:${vertx.version:4.1.1} -//DEPS io.vertx:vertx-db2-client:${vertx.version:4.1.1} -//DEPS io.vertx:vertx-mysql-client:${vertx.version:4.1.1} -//DEPS io.vertx:vertx-unit:${vertx.version:4.1.1} +//DEPS io.vertx:vertx-pg-client:${vertx.version:4.1.2} +//DEPS io.vertx:vertx-db2-client:${vertx.version:4.1.2} +//DEPS io.vertx:vertx-mysql-client:${vertx.version:4.1.2} +//DEPS io.vertx:vertx-unit:${vertx.version:4.1.2} //DEPS org.hibernate.reactive:hibernate-reactive-core:${hibernate-reactive.version:1.0.0.CR6} //DEPS org.assertj:assertj-core:3.19.0 //DEPS junit:junit:4.13.2