diff --git a/storm-core/src/main/java/st/orm/core/template/Query.java b/storm-core/src/main/java/st/orm/core/template/Query.java index 4dcdd940..8161538f 100644 --- a/storm-core/src/main/java/st/orm/core/template/Query.java +++ b/storm-core/src/main/java/st/orm/core/template/Query.java @@ -278,10 +278,18 @@ default List> getRefList(@Nonnull Class type, @Nonnul */ private T singleResult(@Nonnull Stream stream) { try (stream) { - return stream - .reduce((a, b) -> { - throw new NonUniqueResultException("Expected single result, but found more than one."); - }).orElseThrow(() -> new NoResultException("Expected single result, but found none.")); + var iterator = stream.iterator(); + if (!iterator.hasNext()) { + throw new NoResultException("Expected single result, but found none."); + } + T result = iterator.next(); + if (iterator.hasNext()) { + throw new NonUniqueResultException("Expected single result, but found more than one."); + } + if (result == null) { + throw new PersistenceException("Expected single result, but found null. Wrap the field in COALESCE() to provide a non-null default."); + } + return result; } } @@ -289,16 +297,25 @@ private T singleResult(@Nonnull Stream stream) { * Returns the single result of the stream, or an empty optional if there is no result. * * @param stream the stream to get the single result from. - * @return the single result of the stream. * @param the type of the result. + * @return the single result of the stream. * @throws NonUniqueResultException if more than one result. + * @throws PersistenceException if the single row's value is null. */ private Optional optionalResult(@Nonnull Stream stream) { try (stream) { - return stream - .reduce((a, b) -> { - throw new NonUniqueResultException("Expected single result, but found more than one."); - }); + var iterator = stream.iterator(); + if (!iterator.hasNext()) { + return Optional.empty(); + } + T result = iterator.next(); + if (iterator.hasNext()) { + throw new NonUniqueResultException("Expected single result, but found more than one."); + } + if (result == null) { + throw new PersistenceException("Result is null. Wrap the field in COALESCE() to provide a non-null default."); + } + return Optional.of(result); } } } diff --git a/storm-core/src/main/java/st/orm/core/template/QueryBuilder.java b/storm-core/src/main/java/st/orm/core/template/QueryBuilder.java index cdbb970f..1bc1cd73 100644 --- a/storm-core/src/main/java/st/orm/core/template/QueryBuilder.java +++ b/storm-core/src/main/java/st/orm/core/template/QueryBuilder.java @@ -946,29 +946,46 @@ public final List getResultList() { * @return the single result. * @throws NoResultException if there is no result. * @throws NonUniqueResultException if more than one result. - * @throws PersistenceException if the query fails. + * @throws PersistenceException if the single row's value is null, or the query fails. */ public final R getSingleResult() { try (var stream = getResultStream()) { - return stream - .reduce((a, b) -> { - throw new NonUniqueResultException("Expected single result, but found more than one."); - }).orElseThrow(() -> new NoResultException("Expected single result, but found none.")); + var iterator = stream.iterator(); + if (!iterator.hasNext()) { + throw new NoResultException("Expected single result, but found none."); + } + R result = iterator.next(); + if (iterator.hasNext()) { + throw new NonUniqueResultException("Expected single result, but found more than one."); + } + if (result == null) { + throw new PersistenceException("Expected single result, but found null. Wrap the field in COALESCE() to provide a non-null default."); + } + return result; } } /** * Executes the query and returns an optional result. * - * @return the optional result. + * @return the optional result; {@link Optional#empty()} when no row matched. * @throws NonUniqueResultException if more than one result. - * @throws PersistenceException if the query fails. + * @throws PersistenceException if the single row's value is null, or the query fails. */ public final Optional getOptionalResult() { try (var stream = getResultStream()) { - return stream.reduce((a, b) -> { + var iterator = stream.iterator(); + if (!iterator.hasNext()) { + return Optional.empty(); + } + R result = iterator.next(); + if (iterator.hasNext()) { throw new NonUniqueResultException("Expected single result, but found more than one."); - }); + } + if (result == null) { + throw new PersistenceException("Result is null. Wrap the field in COALESCE() to provide a non-null default."); + } + return Optional.of(result); } } diff --git a/storm-kotlin/src/main/kotlin/st/orm/template/Query.kt b/storm-kotlin/src/main/kotlin/st/orm/template/Query.kt index 749b26ae..8499f888 100644 --- a/storm-kotlin/src/main/kotlin/st/orm/template/Query.kt +++ b/storm-kotlin/src/main/kotlin/st/orm/template/Query.kt @@ -20,8 +20,8 @@ import kotlinx.coroutines.stream.consumeAsFlow import st.orm.Data import st.orm.NoResultException import st.orm.NonUniqueResultException +import st.orm.PersistenceException import st.orm.Ref -import java.util.function.Supplier import java.util.stream.Stream import kotlin.reflect.KClass @@ -318,29 +318,49 @@ interface Query { */ private fun singleResult(stream: Stream): T { stream.use { - return stream - .reduce { _, _ -> - throw NonUniqueResultException("Expected single result, but found more than one.") - } - .orElseThrow(Supplier { NoResultException("Expected single result, but found none.") }) + val iterator = stream.iterator() + if (!iterator.hasNext()) { + throw NoResultException("Expected single result, but found none.") + } + val result = iterator.next() + if (iterator.hasNext()) { + throw NonUniqueResultException("Expected single result, but found more than one.") + } + if (result == null) { + throw PersistenceException("Expected single result, but found null. Wrap the field in COALESCE() to provide a non-null default.") + } + return result } } /** - * Returns the single result of the stream, or an empty optional if there is no result. + * Returns the single result of the stream, or `null` if there is no result. + * + * Iterates the stream explicitly rather than using [Stream.reduce] — the standard reduce internally + * calls `Optional.of(element)`, which throws a message-less [NullPointerException] when the only + * element is `null`. The iterator form lets the method detect that case and report it via a typed + * [PersistenceException] with a clear message. * * @param stream the stream to get the single result from. - * @return the single result of the stream. * @param the type of the result. + * @return the single result of the stream, or `null` when no row matched. * @throws NonUniqueResultException if more than one result. + * @throws PersistenceException if the single row's value is SQL NULL. */ private fun optionalResult(stream: Stream): T? { stream.use { - return stream - .reduce { _, _ -> - throw NonUniqueResultException("Expected single result, but found more than one.") - } - .orElse(null) + val iterator = stream.iterator() + if (!iterator.hasNext()) { + return null + } + val result = iterator.next() + if (iterator.hasNext()) { + throw NonUniqueResultException("Expected single result, but found more than one.") + } + if (result == null) { + throw PersistenceException("Result is null. Wrap the field in COALESCE() to provide a non-null default.") + } + return result } } } diff --git a/storm-kotlin/src/main/kotlin/st/orm/template/QueryBuilder.kt b/storm-kotlin/src/main/kotlin/st/orm/template/QueryBuilder.kt index 35a02afd..8aae989e 100644 --- a/storm-kotlin/src/main/kotlin/st/orm/template/QueryBuilder.kt +++ b/storm-kotlin/src/main/kotlin/st/orm/template/QueryBuilder.kt @@ -25,7 +25,6 @@ import st.orm.template.TemplateString.Companion.raw import st.orm.template.TemplateString.Companion.wrap import st.orm.template.impl.create import st.orm.template.impl.createRef -import java.util.function.Supplier import java.util.stream.Stream import kotlin.reflect.KClass @@ -1015,15 +1014,22 @@ interface QueryBuilder { * @return the single result. * @throws NoResultException if there is no result. * @throws NonUniqueResultException if more than one result. - * @throws PersistenceException if the query fails. + * @throws PersistenceException if the single row's value is null, or the query fails. */ get() { resultStream.use { stream -> - return stream - .reduce { _, _ -> - throw NonUniqueResultException("Expected single result, but found more than one.") - } - .orElseThrow(Supplier { NoResultException("Expected single result, but found none.") }) + val iterator = stream.iterator() + if (!iterator.hasNext()) { + throw NoResultException("Expected single result, but found none.") + } + val result = iterator.next() + if (iterator.hasNext()) { + throw NonUniqueResultException("Expected single result, but found more than one.") + } + if (result == null) { + throw PersistenceException("Expected single result, but found null. Wrap the field in COALESCE() to provide a non-null default.") + } + return result } } @@ -1031,16 +1037,24 @@ interface QueryBuilder { /** * Executes the query and returns an optional result. * - * @return the optional result. + * @return the optional result; `null` when no row matched. * @throws NonUniqueResultException if more than one result. - * @throws PersistenceException if the query fails. + * @throws PersistenceException if the single row's value is null, or the query fails. */ get() { resultStream.use { stream -> - return stream.reduce { _, _ -> + val iterator = stream.iterator() + if (!iterator.hasNext()) { + return null + } + val result = iterator.next() + if (iterator.hasNext()) { throw NonUniqueResultException("Expected single result, but found more than one.") } - .orElse(null) + if (result == null) { + throw PersistenceException("Result is null. Wrap the field in COALESCE() to provide a non-null default.") + } + return result } }