Skip to content
Merged
11 changes: 11 additions & 0 deletions Source/Core/Core/RowReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ static bool SkipRows(DbDataReader reader, int skip)

static InvalidOperationException MoreThanOne() => new("Sequence contains more than one element.");

// For the throwing terminals (First, Single) when the query returned no row; thrown after the reader is cleaned up.
internal static InvalidOperationException NoRows(string terminal)
=> new($"Sequence contains no elements: the query returned no rows, and {terminal} needs one.");

#endregion

#region Asynchronous
Expand Down Expand Up @@ -399,6 +403,13 @@ internal readonly struct DictionarySink<T, TKey>(Dictionary<TKey, T> dictionary,
public void Add(T item) => Dictionary.Add(keySelector(item), item);
}

internal readonly struct DictionarySink<T, TKey, TValue>(Dictionary<TKey, TValue> dictionary, Func<T, TKey> keySelector, Func<T, TValue> valueSelector) : IRowSink<T>
where TKey : notnull
{
public Dictionary<TKey, TValue> Dictionary { get; } = dictionary;
public void Add(T item) => Dictionary.Add(keySelector(item), valueSelector(item));
}

internal readonly struct HashSetSink<T>(HashSet<T> set) : IRowSink<T>
{
public HashSet<T> Set { get; } = set;
Expand Down
33 changes: 33 additions & 0 deletions Source/Core/Extensions/IExecuteReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,39 @@ public static ValueTask<ImmutableArray<T>> ToImmutableArrayAsync<T>(
CancellationToken cancellationToken = default)
=> command.Map<T>().SingleOrDefaultAsync(cancellationToken);

/// <summary>
/// Reads the first row, asynchronously; throws when there is none.
/// Equivalent to <c>command.Map&lt;T&gt;().FirstAsync(cancellationToken)</c>.
/// </summary>
/// <inheritdoc cref="ToListAsync{T}(IExecuteReader, CancellationToken)" path="/*[not(self::summary) and not(self::returns) and not(self::param)]"/>
/// <param name="command">The command to read from.</param>
/// <param name="cancellationToken">A token combined with the command's own.</param>
/// <returns>The first row.</returns>
/// <exception cref="InvalidOperationException">
/// There is no row.
/// </exception>
public static ValueTask<T> FirstAsync<T>(
this IExecuteReader command,
CancellationToken cancellationToken = default)
=> command.Map<T>().FirstAsync(cancellationToken);

/// <summary>
/// Reads the only row, asynchronously; throws when there is none
/// or more than one.
/// Equivalent to <c>command.Map&lt;T&gt;().SingleAsync(cancellationToken)</c>.
/// </summary>
/// <inheritdoc cref="ToListAsync{T}(IExecuteReader, CancellationToken)" path="/*[not(self::summary) and not(self::returns) and not(self::param)]"/>
/// <param name="command">The command to read from.</param>
/// <param name="cancellationToken">A token combined with the command's own.</param>
/// <returns>The only row.</returns>
/// <exception cref="InvalidOperationException">
/// There is no row, or more than one.
/// </exception>
public static ValueTask<T> SingleAsync<T>(
this IExecuteReader command,
CancellationToken cancellationToken = default)
=> command.Map<T>().SingleAsync(cancellationToken);

/// <summary>
/// Reads every row into a hash set, asynchronously.
/// Equivalent to <c>command.Map&lt;T&gt;().ToHashSetAsync(comparer, cancellationToken)</c>.
Expand Down
306 changes: 303 additions & 3 deletions Source/Core/Extensions/MappedQueryExtensions.cs

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions Source/Core/IMappedTerminals.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Open.Database.Extensions;

/// <summary>
/// The early-stopping terminals that the types of this library implement as members,
/// so that the extensions on <see cref="IMappedQuery{T}"/> can call them
/// instead of running LINQ over the rows.
/// </summary>
/// <typeparam name="T">The type each row is mapped to.</typeparam>
/// <remarks>
/// Internal on purpose: it is not part of the public API.
/// A type of this library is told apart from another implementation of
/// <see cref="IMappedQuery{T}"/> by this interface, and only the library's own types have it.
/// Through <see cref="IMappedQuery{T}"/> they do what the members do:
/// the library's no-rows message, and the read-through of the later result sets.
/// </remarks>
internal interface IMappedTerminals<T>
{
T First();

T Single();

T? FirstOrDefault();

T? SingleOrDefault();

ValueTask<T> FirstAsync(
CancellationToken cancellationToken = default);

ValueTask<T> SingleAsync(
CancellationToken cancellationToken = default);

ValueTask<T?> FirstOrDefaultAsync(
CancellationToken cancellationToken = default);

ValueTask<T?> SingleOrDefaultAsync(
CancellationToken cancellationToken = default);
}
Loading