diff --git a/src/main/java/org/kiwiproject/jdbc/KiwiJdbc.java b/src/main/java/org/kiwiproject/jdbc/KiwiJdbc.java index 2d4be0bc..e3fe3069 100644 --- a/src/main/java/org/kiwiproject/jdbc/KiwiJdbc.java +++ b/src/main/java/org/kiwiproject/jdbc/KiwiJdbc.java @@ -1,13 +1,16 @@ package org.kiwiproject.jdbc; +import static com.google.common.base.Preconditions.checkState; import static java.util.Objects.isNull; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull; +import static org.kiwiproject.base.KiwiStrings.format; import lombok.experimental.UtilityClass; import org.checkerframework.checker.nullness.qual.Nullable; import org.kiwiproject.base.KiwiPrimitives; import org.kiwiproject.base.KiwiPrimitives.BooleanConversionOption; +import org.kiwiproject.base.KiwiStrings; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -29,6 +32,51 @@ @UtilityClass public class KiwiJdbc { + /** + * Attempt to call {@link ResultSet#next()} on the given ResultSet, and throw an {@link IllegalStateException} + * if the result set was not advanced. + * + * @param rs the ResultSet + * @throws SQLException if a database access error occurs or this method is called on a closed result set + * (copied from {@link ResultSet#next()}) + * @throws IllegalStateException if the result set was not advanced + */ + public static void nextOrThrow(ResultSet rs) throws SQLException { + nextOrThrow(rs, "ResultSet.next() returned false"); + } + + /** + * Attempt to call {@link ResultSet#next()} on the given ResultSet, and throw an {@link IllegalStateException} + * if the result set was not advanced. + * + * @param rs the ResultSet + * @param message the error message in case the result set cannot be advanced + * @throws SQLException if a database access error occurs or this method is called on a closed result set + * (copied from {@link ResultSet#next()}) + * @throws IllegalStateException if the result set was not advanced + */ + public static void nextOrThrow(ResultSet rs, String message) throws SQLException { + checkState(rs.next(), message); + } + + /** + * Attempt to call {@link ResultSet#next()} on the given ResultSet, and throw an {@link IllegalStateException} + * if the result set was not advanced. + * + * @param rs the ResultSet + * @param messageTemplate the error message template in case the result set cannot be advanced, according to how + * {@link KiwiStrings#format(String, Object...)} handles placeholders + * @param args the arguments to be substituted into the message template + * @throws SQLException if a database access error occurs or this method is called on a closed result set + * (copied from {@link ResultSet#next()}) + * @throws IllegalStateException if the result set was not advanced + */ + public static void nextOrThrow(ResultSet rs, String messageTemplate, Object... args) throws SQLException { + if (!rs.next()) { + throw new IllegalStateException(format(messageTemplate, args)); + } + } + /** * Convert the timestamp column given by {@code columnName} in the {@link ResultSet} to milliseconds * from the epoch. diff --git a/src/test/java/org/kiwiproject/jdbc/KiwiJdbcTest.java b/src/test/java/org/kiwiproject/jdbc/KiwiJdbcTest.java index c88455ac..6dc79879 100644 --- a/src/test/java/org/kiwiproject/jdbc/KiwiJdbcTest.java +++ b/src/test/java/org/kiwiproject/jdbc/KiwiJdbcTest.java @@ -1,14 +1,18 @@ package org.kiwiproject.jdbc; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -37,6 +41,84 @@ @DisplayName("KiwiJdbc") class KiwiJdbcTest { + @Nested + class NextOrThrow { + + private ResultSet resultSet; + + @BeforeEach + void setUp() { + resultSet = mock(ResultSet.class); + } + + @Test + void shouldAdvanceResultSet() throws SQLException { + when(resultSet.next()).thenReturn(true); + + assertThatCode(() -> KiwiJdbc.nextOrThrow(resultSet)) + .doesNotThrowAnyException(); + + verify(resultSet, only()).next(); + } + + @Test + void shouldAdvanceResultSet_WithCustomMessageArgument() throws SQLException { + when(resultSet.next()).thenReturn(true); + + assertThatCode(() -> KiwiJdbc.nextOrThrow(resultSet, "failed to advance ResultSet")) + .doesNotThrowAnyException(); + + verify(resultSet, only()).next(); + } + + @Test + void shouldAdvanceResultSet_WithCustomMessageTemplateAndArguments() throws SQLException { + when(resultSet.next()).thenReturn(true); + + assertThatCode(() -> KiwiJdbc.nextOrThrow(resultSet, "{} with id {} was not found", "Order", "12345ABC")) + .doesNotThrowAnyException(); + + verify(resultSet, only()).next(); + } + + @Test + void shouldThrowIllegalState_WhenNextReturnsFalse() throws SQLException { + when(resultSet.next()).thenReturn(false); + + assertThatIllegalStateException() + .isThrownBy(() -> KiwiJdbc.nextOrThrow(resultSet)) + .withMessage("ResultSet.next() returned false"); + + verify(resultSet, only()).next(); + } + + @Test + void shouldThrowIllegalState_WithCustomMessage_WhenNextReturnsFalse() throws SQLException { + when(resultSet.next()).thenReturn(false); + + assertThatIllegalStateException() + .isThrownBy(() -> KiwiJdbc.nextOrThrow(resultSet, "record was not found")) + .withMessage("record was not found"); + + verify(resultSet, only()).next(); + } + + @ParameterizedTest + @ValueSource(strings = { + "{} with id {} was not found", + "%s with id %s was not found" + }) + void shouldThrowIllegalState_WithCustomMessageTemplate_WhenNextReturnsFalse(String template) throws SQLException { + when(resultSet.next()).thenReturn(false); + + assertThatIllegalStateException() + .isThrownBy(() -> KiwiJdbc.nextOrThrow(resultSet, template, "Item", 42)) + .withMessage("Item with id 42 was not found"); + + verify(resultSet, only()).next(); + } + } + @Nested class EpochMillisFromTimestamp {