Skip to content

Commit

Permalink
Refine exception specification when accessing items by name/by index.
Browse files Browse the repository at this point in the history
We now define IndexOutOfBoundsException to signal out of range indexes for by-index access and NoSuchElementException to signal unknown/illegal identifiers for by-name access to columns, out parameters, and bind values.

[resolves r2dbc#184][r2dbc#240]
Signed-off-by: Mark Paluch <[email protected]>
  • Loading branch information
mp911de committed Sep 9, 2021
1 parent f41ed12 commit f55bb5d
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 6 deletions.
14 changes: 13 additions & 1 deletion r2dbc-spec/src/main/asciidoc/exceptions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ R2DBC defines the following general exceptions:
[[exceptions.iae]]
=== `IllegalArgumentException`

Drivers throw `IllegalArgumentException` if a method has been received an illegal or inappropriate argument (such as values that are out of bounds or an expected parameter is `null`).
Drivers throw `IllegalArgumentException` if a method has received an illegal or inappropriate argument (such as values that are invalid or an expected parameter is `null`).
This exception is a generic exception that is not associated with an error code or an `SQLState`.

[[exceptions.iob]]
=== `IndexOutOfBoundsException`

Drivers throw `IndexOutOfBoundsException` if a method has received an illegal or inappropriate index value (such as negative values or indexes that are greater or equal to the number of elements).
This exception is a generic exception that is not associated with an error code or an `SQLState`.

[[exceptions.ise]]
Expand All @@ -29,6 +35,12 @@ This exception is a generic exception that is not associated with an error code
Drivers throw `IllegalStateException` if a method has received an argument that is invalid in the current state or when an argument-less method is invoked in a state that does not allow execution in the current state (such as interacting with a closed connection object).
This exception is a generic exception that is not associated with an error code or an `SQLState`.

[[exceptions.nse]]
=== `NoSuchElementException`

Drivers throw `NoSuchElementException` if a method has received a column/parameter name that doesn't exist in the collection of items.
This exception is a generic exception that is not associated with an error code or an `SQLState`.

[[exceptions.nsoe]]
=== `NoSuchOptionException`

Expand Down
51 changes: 49 additions & 2 deletions r2dbc-spi-test/src/main/java/io/r2dbc/spi/test/TestKit.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.concurrent.Callable;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
Expand Down Expand Up @@ -318,7 +320,7 @@ default void bindFails() {
assertThrows(IndexOutOfBoundsException.class, () -> statement.bind(99, ""), "bind(nonexistent-index, null) should fail");
assertThrows(IllegalArgumentException.class, () -> bind(statement, getIdentifier(0), null), "bind(identifier, null) should fail");
assertThrows(IllegalArgumentException.class, () -> bind(statement, getIdentifier(0), Class.class), "bind(identifier, Class.class) should fail");
assertThrows(IllegalArgumentException.class, () -> statement.bind("unknown", ""), "bind(unknown-placeholder, \"\") should fail");
assertThrows(NoSuchElementException.class, () -> statement.bind("unknown-placeholder", ""), "bind(unknown-placeholder, \"\") should fail");
return Mono.empty();
},
Connection::close)
Expand Down Expand Up @@ -492,14 +494,20 @@ default void columnMetadata() {
.execute())
.flatMap(result -> {
return result.map((row, rowMetadata) -> {
return Arrays.asList(rowMetadata.contains("value"), rowMetadata.contains("VALUE"));
return Arrays.asList(rowMetadata.contains("value"), rowMetadata.contains("VALUE"),
captureException(() -> rowMetadata.getColumnMetadata(-1)),
captureException(() -> rowMetadata.getColumnMetadata(100)),
captureException(() -> rowMetadata.getColumnMetadata("unknown")));
});
})
.flatMapIterable(Function.identity()),
Connection::close)
.as(StepVerifier::create)
.expectNext(true).as("rowMetadata.contains(value)")
.expectNext(true).as("rowMetadata.contains(VALUE)")
.expectNextMatches(IndexOutOfBoundsException.class::isInstance).as("getColumnMetadata(-1) throws IndexOutOfBoundsException")
.expectNextMatches(IndexOutOfBoundsException.class::isInstance).as("getColumnMetadata(100) throws IndexOutOfBoundsException")
.expectNextMatches(NoSuchElementException.class::isInstance).as("getColumnMetadata(unknown) throws NoSuchElementException")
.verifyComplete();
}

Expand All @@ -522,6 +530,27 @@ default void rowMetadata() {
.verifyComplete();
}

@Test
default void row() {
getJdbcOperations().execute(expand(TestStatement.INSERT_TWO_COLUMNS));

Flux.usingWhen(getConnectionFactory().create(),
connection -> Flux.from(connection

.createStatement(expand(TestStatement.SELECT_VALUE_ALIASED_COLUMNS))
.execute())
.flatMap(result -> result.map(readable -> Arrays.asList(captureException(() -> readable.get(-1)),
captureException(() -> readable.get(100)),
captureException(() -> readable.get("unknown")))))
.flatMapIterable(Function.identity()),
Connection::close)
.as(StepVerifier::create)
.expectNextMatches(IndexOutOfBoundsException.class::isInstance).as("get(-1) throws IndexOutOfBoundsException")
.expectNextMatches(IndexOutOfBoundsException.class::isInstance).as("get(100) throws IndexOutOfBoundsException")
.expectNextMatches(NoSuchElementException.class::isInstance).as("get(unknown) throws NoSuchElementException")
.verifyComplete();
}

@Test
default void compoundStatement() {
getJdbcOperations().execute(expand(TestStatement.INSERT_VALUE100));
Expand Down Expand Up @@ -868,6 +897,24 @@ static <T> Collection<T> collectionOf(T... values) {
return new HashSet<>(Arrays.asList(values));
}

/**
* Returns an {@link Exception} from a {@link Callable}.
*
* @param throwingCallable
* @return
* @throws IllegalStateException if {@code throwingCallable} did not throw an exception.
*/
static Exception captureException(Callable<?> throwingCallable) {

try {
throwingCallable.call();
} catch (Exception e) {
return e;
}

throw new IllegalStateException("Callable did not throw an exception.");
}

/**
* Enumeration of TCK statements. Can be customized by overriding {@link #expand(TestStatement, Object...)}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public interface OutParametersMetadata {
*
* @param index the out parameter index starting at 0
* @return the {@link OutParameterMetadata} for one out parameter
* @throws ArrayIndexOutOfBoundsException if the {@code index} is less than zero or greater than the number of available out parameters.
* @throws IndexOutOfBoundsException if {@code index} is out of range (negative or equals/exceeds {@code getParameterMetadatas().size()})
*/
OutParameterMetadata getParameterMetadata(int index);

Expand Down
8 changes: 7 additions & 1 deletion r2dbc-spi/src/main/java/io/r2dbc/spi/Readable.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package io.r2dbc.spi;

import java.util.NoSuchElementException;

/**
* Represents a readable object, for example a set of columns or {@code OUT} parameters from a database query, later on referred to as items.
* Values can for columns or {@code OUT} parameters be either retrieved by specifying a name or the index.
Expand Down Expand Up @@ -44,6 +46,7 @@ public interface Readable {
*
* @param index the index of the parameter starting at {@code 0}
* @return the value. Value can be {@code null}.
* @throws IndexOutOfBoundsException if {@code index} if the index is out of range (negative or equals/exceeds the number of readable objects)
*/
@Nullable
default Object get(int index) {
Expand All @@ -57,7 +60,8 @@ default Object get(int index) {
* @param type the type of item to return. This type must be assignable to, and allows for variance.
* @param <T> the type of the item being returned.
* @return the value. Value can be {@code null}.
* @throws IllegalArgumentException if {@code type} is {@code null}
* @throws IllegalArgumentException if {@code type} is {@code null}
* @throws IndexOutOfBoundsException if {@code index} is out of range (negative or equals/exceeds the number of readable objects)
*/
@Nullable
<T> T get(int index, Class<T> type);
Expand All @@ -69,6 +73,7 @@ default Object get(int index) {
* @param name the name
* @return the value. Value can be {@code null}.
* @throws IllegalArgumentException if {@code name} is {@code null}
* @throws NoSuchElementException if {@code name} is not a known readable column or out parameter
*/
@Nullable
default Object get(String name) {
Expand All @@ -83,6 +88,7 @@ default Object get(String name) {
* @param <T> the type of the item being returned.
* @return the value. Value can be {@code null}.
* @throws IllegalArgumentException if {@code name} or {@code type} is {@code null}
* @throws NoSuchElementException if {@code name} is not a known readable column or out parameter
*/
@Nullable
<T> T get(String name, Class<T> type);
Expand Down
2 changes: 1 addition & 1 deletion r2dbc-spi/src/main/java/io/r2dbc/spi/RowMetadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public interface RowMetadata {
*
* @param index the column index starting at 0
* @return the {@link ColumnMetadata} for one column in this row
* @throws ArrayIndexOutOfBoundsException if the {@code index} is less than zero or greater than the number of available columns.
* @throws IndexOutOfBoundsException if {@code index} is out of range (negative or equals/exceeds {@code getColumnMetadatas().size()})
*/
ColumnMetadata getColumnMetadata(int index);

Expand Down
4 changes: 4 additions & 0 deletions r2dbc-spi/src/main/java/io/r2dbc/spi/Statement.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import org.reactivestreams.Publisher;

import java.util.NoSuchElementException;

/**
* A statement that can be executed multiple times in a prepared and optimized way. Bound parameters can be either scalar values (using type inference for the database parameter type) or
* {@link Parameter} objects.
Expand Down Expand Up @@ -64,6 +66,7 @@ public interface Statement {
* @param value the value to bind
* @return this {@link Statement}
* @throws IllegalArgumentException if {@code name} or {@code value} is {@code null}
* @throws NoSuchElementException if {@code name} is not a known name to bind
*/
Statement bind(String name, Object value);

Expand All @@ -85,6 +88,7 @@ public interface Statement {
* @param type the type of null value
* @return this {@link Statement}
* @throws IllegalArgumentException if {@code name} or {@code type} is {@code null}
* @throws NoSuchElementException if {@code name} is not a known name to bind
*/
Statement bindNull(String name, Class<?> type);

Expand Down

0 comments on commit f55bb5d

Please sign in to comment.