Skip to content

Commit f0a7d24

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 3d12772 commit f0a7d24

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.JdbcSessionContext;
2830
import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor;
2931
import org.hibernate.sql.exec.spi.ExecutionContext;
@@ -42,7 +44,6 @@ public class ReactiveDeferredResultSetAccess extends DeferredResultSetAccess imp
4244

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

4748
private final ExecutionContext executionContext;
4849

@@ -60,7 +61,6 @@ public ReactiveDeferredResultSetAccess(
6061
super( jdbcSelect, jdbcParameterBindings, executionContext, statementCreator );
6162
this.executionContext = executionContext;
6263
this.sqlStatementLogger = executionContext.getSession().getJdbcServices().getSqlStatementLogger();
63-
this.statementCreator = statementCreator;
6464
}
6565

6666
@Override
@@ -163,17 +163,33 @@ private CompletionStage<ResultSet> executeQuery() {
163163
eventListenerManager.jdbcExecuteStatementStart();
164164
return connection()
165165
.selectJdbc( sql, parameters )
166+
.thenCompose( this::validateResultSet )
166167
.whenComplete( (resultSet, throwable) -> {
167168
// FIXME: I don't know if this event makes sense for Vert.x
168169
eventListenerManager.jdbcExecuteStatementEnd();
169170
sqlStatementLogger.logSlowQuery( getFinalSql(), executeStartNanos, context() );
170171
} )
171172
.thenCompose( this::reactiveSkipRows )
172-
.handle( this::convertException );
173+
.handle( CompletionStages::handle )
174+
.thenCompose( handler -> handler.hasFailed()
175+
? convertException( resultSet, handler.getThrowable() )
176+
: handler.getResultAsCompletionStage()
177+
);
173178
} )
174179
.whenComplete( (o, throwable) -> logicalConnection.afterStatement() );
175180
}
176181

182+
private CompletionStage<ResultSet> validateResultSet(ResultSet resultSet) {
183+
try {
184+
return resultSet.getMetaData().getColumnCount() == 0
185+
? failedFuture( LOG.noResultException( getFinalSql() ) )
186+
: completedFuture( resultSet );
187+
}
188+
catch (SQLException e) {
189+
throw new RuntimeException( e );
190+
}
191+
}
192+
177193
private ResultSet saveResultSet(ResultSet resultSet) {
178194
this.resultSet = resultSet;
179195
return saveColumnCount( resultSet );
@@ -189,18 +205,29 @@ private ReactiveConnection connection() {
189205
return ( (ReactiveConnectionSupplier) executionContext.getSession() ).getReactiveConnection();
190206
}
191207

192-
private ResultSet convertException(ResultSet resultSet, Throwable throwable) {
193-
// FIXME: Vert.x will probably throw another exception. Check this.
194-
if ( throwable instanceof SQLException) {
195-
throw executionContext.getSession().getJdbcServices()
196-
.getSqlExceptionHelper()
197-
// FIXME: Add this to the logger?
198-
.convert( (SQLException) throwable, "Exception executing SQL [" + getFinalSql() + "]" );
199-
}
208+
private <T> CompletionStage<T> convertException(T object, Throwable throwable) {
200209
if ( throwable != null ) {
201-
throw new HibernateException( throwable );
210+
Throwable cause = throwable;
211+
if ( throwable instanceof CompletionException ) {
212+
cause = throwable.getCause();
213+
}
214+
// I doubt this is ever going to happen because Vert.x is not going to throw an SQLException
215+
if ( cause instanceof SQLException ) {
216+
return failedFuture( executionContext
217+
.getSession().getJdbcServices()
218+
.getSqlExceptionHelper()
219+
.convert(
220+
(SQLException) cause,
221+
"Exception executing SQL [" + getFinalSql() + "]"
222+
)
223+
);
224+
}
225+
if ( cause instanceof HibernateException ) {
226+
return failedFuture( cause );
227+
}
228+
return failedFuture( new HibernateException( cause ) );
202229
}
203-
return resultSet;
230+
return completedFuture( object );
204231
}
205232

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

0 commit comments

Comments
 (0)