Skip to content

feat(querying): row projections, keyset paging and capped counts - #65

Merged
AhmadRAbuhussein merged 2 commits into
muhannad/searchy-parametersfrom
muhannad/query-shaping
Oct 20, 2026
Merged

AhmadRAbuhussein merged 2 commits into
muhannad/searchy-parametersfrom
muhannad/query-shaping

Conversation

@mmalkhatib

Copy link
Copy Markdown
Contributor

Stacked on #64; review that first.

Three small provider-neutral helpers over IQueryable<T>, for list endpoints where the caller chooses what to read. They take already-resolved expressions and know nothing about requests, JSON or permissions.

  • RowProjection.Of<T>(columns) / RowProjection.Many<T,TChild>(...): a Select to object[] rows, including nested collection columns (ordered, capped). PostgreSQL reads only the chosen columns, including inside a collection. The shape depends on which columns are chosen, not on values, so each choice compiles once.
  • AfterKeyset / OrderByKeyset: keyset paging. (k1 > v1) OR (k1 = v1 AND k2 > v2) …, with < for descending keys. Positions are parameters; strings compare through string.Compare and enums through their underlying type. Nullable keys are refused, since NULL comparisons would drop rows. End the keys with a unique column.
  • ToPageAsync(size): a page plus hasMore from one query that reads one extra row. CountUpToAsync(limit): count(*) over LIMIT limit+1, for '10,000+'.

Tests (PostgreSQL 17, Testcontainers), 10 new, 27/27 in the project:

  • the SELECT list holds only the chosen columns;
  • collection columns are ordered and capped;
  • the same columns compile once across new expression trees;
  • keyset paging over four orderings (unique key, duplicate status, descending money, string + date + id) visits every row exactly once, in order, with no new compilation per position;
  • nullable keys are refused;
  • hasMore and capped counts are right at the boundaries.

Unit tests still pass, 24/24.

Three provider-neutral helpers for list endpoints that let the caller choose
what to read:

- RowProjection builds a Select to object[] rows from column expressions,
  with nested collection columns (ordered, capped). The query reads only
  the chosen columns, and its shape depends on which columns are chosen,
  never on values, so each choice compiles once.
- AfterKeyset/OrderByKeyset page by the last row's key values instead of
  Skip, so a deep page costs the same as the first. Positions are sent as
  parameters; nullable keys are refused, since SQL comparisons with NULL
  would drop rows.
- ToPageAsync reads one row beyond the page to say whether more follow,
  and CountUpToAsync stops counting at a limit, for lists that don't need
  an exact count of millions of rows.

Integration tests on PostgreSQL 17 cover column pruning, collection
columns, single compilation per shape and keyset completeness over
duplicate keys.
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review: 8
  • ^main$
  • ^master$
  • ^staging$
  • ^development$
  • ^gigstaging$
  • ^develop$
  • ^releases$
  • ^releases/r10.0$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: simplify9/coderabbit/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 3ded9d92-f50d-48ef-b465-91c46a3c123a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

QueryExpressions.Parameter turns a value into an expression EF Core sends as
a parameter (the same capture Searchy uses), Convert exposes the invariant
conversion, and TextMatch exposes the package's text match, now with a
suffix variant as well as prefix and contains. For code that builds its own
filter expressions without going through SearchyRequest.
@AhmadRAbuhussein
AhmadRAbuhussein merged commit c0a247b into muhannad/searchy-parameters Oct 20, 2026
3 checks passed
AhmadRAbuhussein added a commit that referenced this pull request Oct 20, 2026
feat(querying): row projections, keyset paging and capped counts (#65 onto main)
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.

2 participants