Skip to content

Select on IMappedQuery<T> composes a projection without running the query (query factories) - #124

Merged
electricessence merged 5 commits into
masterfrom
feat/lazy-select
Nov 12, 2026
Merged

electricessence merged 5 commits into
masterfrom
feat/lazy-select

Conversation

@electricessence

Copy link
Copy Markdown
Collaborator

Select on IMappedQuery<T> composes a projection without running the query, so a data class can return the query and let the caller pick the terminal.

// The data class returns the deferred query, already in its final type:
public IMappedQuery<PostSummary> ByAuthor(int author)
    => connectionFactory
        .Command("select id, title from posts where author = @author")
        .AddParam(author)
        .Map<Post>()
        .Select(p => new PostSummary(p.Id, p.Title));

// The caller decides what to do with it:
List<PostSummary> all = await posts.ByAuthor(42).ToListAsync(ct);
PostSummary? first = await posts.ByAuthor(42).FirstOrDefaultAsync(ct);

What

  • public static IMappedQuery<TResult> Select<T, TResult>(this IMappedQuery<T> query, Func<T, TResult> selector) is the only new public member. The type that implements it is internal.
  • Nothing runs until a terminal runs. The projection sits on top of Read and ReadAsync, so every existing terminal on IMappedQuery<T> works on the result, sync and async, at every level (expressive command, raw command, reader, table). The connection ownership, the read-through of later result sets and the SingleResult() behavior of the source are unchanged.
  • The selector runs once per row, in row order, only while a terminal enumerates. An early exit (FirstOrDefault, a Take) runs it for the rows read and releases the reader; a selector that throws or a cancellation releases it too. Chained Select composes and uses one reader. Skip and Take are not on the interface, so they apply to the source before Select.
  • Cost: a fixed cost per terminal call, no per-row allocation beyond the selector's own.

Known limitations (follow-up)

  • A terminal on a projected query is the IMappedQuery<T> extension form. ToArray there uses LINQ's ToArray, not the library's pooled buffer, and an early-stopping terminal (FirstOrDefault) does not read through later result sets (the same as the interface extensions do today). Both are fixed together in a follow-up so the projection can hand its terminals to the source.

Docs

  • mapping.md has a "Query factories" section with a compiled sample; whats-new-11.md has an entry.

Tests

  • 155 cases: deferral at five levels (nothing runs on Select), every terminal sync and async, selector once per row and in order, early exit with the reader released, selector throws, cancellation (before, during, command token, token handed to the handler), release of the reader, command and connection, chained Select, two terminals run the command twice, a default struct, null arguments, a fixed-cost allocation guard.

Checks

  • Release and Debug builds: 0 warnings, 0 errors. Tests: 9832 of 9832 pass in both. Samples: 107 of 107 checks pass. Style and analyzers: 0 findings. DocFX: 0 warnings.

🤖 Generated with Claude Code

electricessence and others added 5 commits November 12, 2026 04:19
…uery

The result is an IMappedQuery<TResult> built over Read and ReadAsync, so a data class can return the query
and the caller picks the terminal. Nothing runs until a terminal does.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…he concrete query type for the helper

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@electricessence
electricessence merged commit 0a6ef97 into master Nov 12, 2026
2 checks passed
@electricessence
electricessence deleted the feat/lazy-select branch November 12, 2026 15:25
electricessence added a commit that referenced this pull request Nov 12, 2026
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
electricessence added a commit that referenced this pull request Nov 12, 2026
Conflicts: First/Single and Select are both added at the end of MappedQueryExtensions, so both are kept; the
public-API list, the notes and the samples keep both sides.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant