Skip to content

Commit 26d96e9

Browse files
committed
[#1792] Throw an exception if there is no result
When running a native query that doesn't return any result (for example, the creation of a stored procedure), `io.vertx.sqlclient.SqlResult#columnsNames` will be null. This commit doesn two things: 1. add null check to avoid a NPE if `.columnsNames` returns null; 2. Throw an HibernateException if the user call getSingleResultOrNull but the query doesn't return any result. Note that we could return null in this situation, but we've decided to be consistent with Hibernate ORM, where the JDBC driver will throw the following exception (with PostgreSQL at least): ``` Caused by: org.postgresql.util.PSQLException: No results were returned by the query. at org.postgresql.jdbc.PgPreparedStatement.executeQuery(PgPreparedStatement.java:135) at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.executeQuery(DeferredResultSetAccess.java:240) ```
1 parent db4c3b5 commit 26d96e9

File tree

2 files changed

+62
-20
lines changed

2 files changed

+62
-20
lines changed

hibernate-reactive-core/src/main/java/org/hibernate/reactive/adaptor/impl/ResultSetAdaptor.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@
2828
import java.time.LocalDateTime;
2929
import java.time.LocalTime;
3030
import java.util.Calendar;
31+
import java.util.List;
3132
import java.util.Map;
3233
import java.util.NoSuchElementException;
33-
import java.util.Objects;
3434
import java.util.function.Function;
3535

3636
import org.hibernate.engine.jdbc.BlobProxy;
@@ -42,6 +42,9 @@
4242
import io.vertx.sqlclient.RowSet;
4343
import io.vertx.sqlclient.desc.ColumnDescriptor;
4444

45+
import static java.util.Collections.emptyList;
46+
import static java.util.Objects.requireNonNull;
47+
4548
/**
4649
* An adaptor that allows Hibernate core code which expects a JDBC
4750
* {@code ResultSet} to read values from Vert.x's {@code RowSet}.
@@ -54,6 +57,7 @@ public class ResultSetAdaptor implements ResultSet {
5457
private boolean wasNull;
5558

5659
public ResultSetAdaptor(RowSet<Row> rows) {
60+
requireNonNull( rows );
5761
this.iterator = rows.iterator();
5862
this.rows = rows;
5963
}
@@ -199,15 +203,26 @@ public String getString(String columnLabel) {
199203
}
200204

201205
private <T> T caseInsensitiveGet(String columnLabel, Function<String, T> produce) {
202-
for ( String columnName : rows.columnsNames() ) {
206+
for ( String columnName : getColumnsNames() ) {
203207
if ( columnName.equalsIgnoreCase( columnLabel ) ) {
204208
return produce.apply( columnName );
205209
}
206210
}
211+
207212
// Same error thrown by io.vertx.sqlclient.Row when it doesn't find the label
208213
throw new NoSuchElementException( "Column " + columnLabel + " does not exist" );
209214
}
210215

216+
/**
217+
* rows.columnsNames() might return null for some databases.
218+
* For example, when creating a stored procedure in PostgreSQL using a native query.
219+
*
220+
* @return A list of column names or an empty list.
221+
*/
222+
private List<String> getColumnsNames() {
223+
return rows.columnsNames() == null ? emptyList() : rows.columnsNames();
224+
}
225+
211226
@Override
212227
public boolean getBoolean(String columnLabel) {
213228
try {
@@ -368,7 +383,7 @@ public ResultSetMetaData getMetaData() {
368383
return new ResultSetMetaData() {
369384
@Override
370385
public int getColumnCount() {
371-
return rows.columnsNames().size();
386+
return getColumnsNames() == null ? 0 : getColumnsNames().size();
372387
}
373388

374389
@Override
@@ -379,12 +394,12 @@ public int getColumnType(int column) {
379394

380395
@Override
381396
public String getColumnLabel(int column) {
382-
return rows.columnsNames().get( column - 1 );
397+
return getColumnsNames().get( column - 1 );
383398
}
384399

385400
@Override
386401
public String getColumnName(int column) {
387-
return rows.columnsNames().get( column - 1 );
402+
return getColumnsNames().get( column - 1 );
388403
}
389404

390405
@Override
@@ -502,7 +517,7 @@ public Object getObject(String columnLabel) {
502517
public int findColumn(String columnLabel) {
503518
// JDBC parameters index start from 1
504519
int index = 1;
505-
for ( String column : rows.columnsNames() ) {
520+
for ( String column : getColumnsNames() ) {
506521
// Some dbs, like Oracle and Db2, return the column names always in uppercase
507522
if ( column.equalsIgnoreCase( columnLabel ) ) {
508523
return index;
@@ -865,7 +880,7 @@ private static class RowIdAdaptor implements RowId {
865880
private final Buffer buffer;
866881

867882
private RowIdAdaptor(Buffer buffer) {
868-
Objects.requireNonNull( buffer );
883+
requireNonNull( buffer );
869884
this.buffer = buffer;
870885
}
871886

hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDeferredResultSetAccess.java

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.sql.ResultSet;
1111
import java.sql.ResultSetMetaData;
1212
import java.sql.SQLException;
13+
import java.util.concurrent.CompletionException;
1314
import java.util.concurrent.CompletionStage;
1415
import java.util.function.Function;
1516

@@ -24,6 +25,7 @@
2425
import org.hibernate.reactive.pool.ReactiveConnection;
2526
import org.hibernate.reactive.pool.impl.Parameters;
2627
import org.hibernate.reactive.session.ReactiveConnectionSupplier;
28+
import org.hibernate.reactive.util.impl.CompletionStages;
2729
import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor;
2830
import org.hibernate.sql.exec.spi.ExecutionContext;
2931
import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect;
@@ -41,7 +43,6 @@ public class ReactiveDeferredResultSetAccess extends DeferredResultSetAccess imp
4143

4244
private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() );
4345
private final SqlStatementLogger sqlStatementLogger;
44-
private final Function<String, PreparedStatement> statementCreator;
4546

4647
private final ExecutionContext executionContext;
4748

@@ -59,7 +60,6 @@ public ReactiveDeferredResultSetAccess(
5960
super( jdbcSelect, jdbcParameterBindings, executionContext, statementCreator );
6061
this.executionContext = executionContext;
6162
this.sqlStatementLogger = executionContext.getSession().getJdbcServices().getSqlStatementLogger();
62-
this.statementCreator = statementCreator;
6363
}
6464

6565
@Override
@@ -159,17 +159,33 @@ private CompletionStage<ResultSet> executeQuery() {
159159
eventListenerManager.jdbcExecuteStatementStart();
160160
return connection()
161161
.selectJdbc( sql, parameters )
162+
.thenCompose( this::validateResultSet )
162163
.whenComplete( (resultSet, throwable) -> {
163164
// FIXME: I don't know if this event makes sense for Vert.x
164165
eventListenerManager.jdbcExecuteStatementEnd();
165166
sqlStatementLogger.logSlowQuery( getFinalSql(), executeStartNanos );
166167
} )
167168
.thenCompose( this::reactiveSkipRows )
168-
.handle( this::convertException );
169+
.handle( CompletionStages::handle )
170+
.thenCompose( handler -> handler.hasFailed()
171+
? convertException( resultSet, handler.getThrowable() )
172+
: handler.getResultAsCompletionStage()
173+
);
169174
} )
170175
.whenComplete( (o, throwable) -> logicalConnection.afterStatement() );
171176
}
172177

178+
private CompletionStage<ResultSet> validateResultSet(ResultSet resultSet) {
179+
try {
180+
return resultSet.getMetaData().getColumnCount() == 0
181+
? failedFuture( LOG.noResultException( getFinalSql() ) )
182+
: completedFuture( resultSet );
183+
}
184+
catch (SQLException e) {
185+
throw new RuntimeException( e );
186+
}
187+
}
188+
173189
private ResultSet saveResultSet(ResultSet resultSet) {
174190
this.resultSet = resultSet;
175191
return saveColumnCount( resultSet );
@@ -185,18 +201,29 @@ private ReactiveConnection connection() {
185201
return ( (ReactiveConnectionSupplier) executionContext.getSession() ).getReactiveConnection();
186202
}
187203

188-
private ResultSet convertException(ResultSet resultSet, Throwable throwable) {
189-
// FIXME: Vert.x will probably throw another exception. Check this.
190-
if ( throwable instanceof SQLException) {
191-
throw executionContext.getSession().getJdbcServices()
192-
.getSqlExceptionHelper()
193-
// FIXME: Add this to the logger?
194-
.convert( (SQLException) throwable, "Exception executing SQL [" + getFinalSql() + "]" );
195-
}
204+
private <T> CompletionStage<T> convertException(T object, Throwable throwable) {
196205
if ( throwable != null ) {
197-
throw new HibernateException( throwable );
206+
Throwable cause = throwable;
207+
if ( throwable instanceof CompletionException ) {
208+
cause = throwable.getCause();
209+
}
210+
// I doubt this is ever going to happen because Vert.x is not going to throw an SQLException
211+
if ( cause instanceof SQLException ) {
212+
return failedFuture( executionContext
213+
.getSession().getJdbcServices()
214+
.getSqlExceptionHelper()
215+
.convert(
216+
(SQLException) cause,
217+
"Exception executing SQL [" + getFinalSql() + "]"
218+
)
219+
);
220+
}
221+
if ( cause instanceof HibernateException ) {
222+
return failedFuture( cause );
223+
}
224+
return failedFuture( new HibernateException( cause ) );
198225
}
199-
return resultSet;
226+
return completedFuture( object );
200227
}
201228

202229
private CompletionStage<ResultSet> reactiveSkipRows(ResultSet resultSet) {

0 commit comments

Comments
 (0)