perf: defer unused joined-field lookups to the paginated page - #18
Merged
Conversation
Query.List applied every @joined lookup to the full stream before the filter, the count() and the list route's slice(), so list endpoints paid for all joined groups across the entire matched set (twice, counting the count aggregation) even when the request neither sorted nor filtered by them. Split joined groups the same way computed fields already are: groups referenced by filters (or forced in by a filtered computed field, whose expression may read joined fields) stay before the filter and count; groups needed only for sorting (or whose local key a later Foreign lookup replaces) are applied after count() and before orderBy, so their lookups never enter the count pipeline; the remaining display-only groups are resolved by the list route after slice(), so their lookups only run on the returned page. Joined() gains an optional `only` set and the new Query.DeferredJoinedFields() exposes the deferred names to the route. Left-join lookups never change the row count, so totals and responses are unchanged. Refs #17
Materialize all deferred joined fields on the page whenever a display-only computed field is resolved post-slice, since its expression may read a joined field that is not itself plucked. Mirrors the conservative rule Query.List already applies for filter- and sort-materialized computed fields; the final pluck() strips any fields the response did not request. Adds a regression test: a listable computed field whose expression reads a non-listable joined field previously evaluated against a missing field and returned null.
Member
Author
|
@greptile review |
Upd4ting
approved these changes
Jul 13, 2026
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Query.Listnow splits@Joinedlookups three ways, matching the pattern already used for computed fields: groups referenced by an active filter stay before the$match/count(), sort-only groups (and groups whoselocalKeya laterForeignlookup would overwrite) are applied aftercount()but beforeorderBy, and all remaining display-only groups are excluded from the streamed query entirely.listroute defers those display-only joined lookups to after.slice(), so they run only on the returned page instead of the entire matched set. A newQuery.DeferredJoinedFields()tells routes which fields were deferred, andQuery.Joined()gained an optional, backward-compatibleonlyparameter.@Computedexpression may read a joined field, any case where a computed field is materialized (filter, sort, or post-slice display) conservatively pulls in the relevant joined groups first, preserving semantics; the finalpluck()strips anything the response did not request.totalare unchanged: the deferred lookups are left joins (count-preserving), and joined values on the returned page are identical to before.Performance impact
Before: every
@Joinedfield triggered a per-rowlookupacross the entire result set on every list call — including the duplicate pass insidecount()— even when the joined fields were not filtered, sorted, or even plucked. For a table with N matching rows, J joined groups, and a page size of L, that wasO(2 x N x J)lookups per request.After: display-only joined groups run
O(L x J)lookups (page only, never in the count pipeline), sort-only groups runO(N x J)once (excluded fromcount()), and only filter-referenced groups keep the previous pre-match behavior, which is required for correctness. For the common case (joined fields shown but not filtered/sorted), a list of 100k rows withlimit=10goes from ~200k lookups per joined group to 10.Fixes #17
Testing
pnpm run build(tsc) passes;pnpm run lint(biome) clean.pnpm test: 75/75 passing, including a new pagination test (deferred joined fields on a sliced page) and a new regression test (listable computed field whose expression reads a non-listable joined field), the latter added after adversarial review caught the gap. The regression test was differentially verified: it fails (display=null) without the follow-up commit and passes with it.This fix was generated by an automated AI performance review pipeline (multi-agent fix + adversarial pre-PR review).
Greptile Summary
This PR optimizes
Query.Listby deferring display-only@Joinedfield lookups to after pagination, so those lookups run against the page (e.g., 10 rows) rather than the entire matched set. A new three-way split (filter/sort/ deferred) mirrors the existing pattern for computed fields, and the default list route applies deferred joins onqueryPagedafterslice().splitJoinedFieldscategorizes each joined group into pre-count (filter-required), post-count (sort-required or localKey-is-foreign), or deferred; the conservative rule pulls every group intosortwhen any computed sort or filter is active.Query.Listnow returns a three-tuple[stream, total, deferredJoined]; the third element is consumed byDefaultRoutes.Listto applyQuery.Joined(…, displayJoined)on the sliced page only.pluck().Confidence Score: 5/5
Safe to merge — the optimization is logically sound and fully backward-compatible, with no changes to externally visible response shape or total counts.
The three-way split of joined groups (filter / sort / deferred) is implemented correctly across all edge cases: computed filters conservatively pull every group pre-count, groups whose localKey is overwritten by a Foreign lookup are moved to sort rather than deferred, and the materializeAll guard in the list route ensures computed fields that read joined data always find those fields materialized before the expression runs. The return-type extension of Query.List to a three-tuple is backward-compatible with existing destructuring. Tests cover both the happy-path pagination case and the regression scenario that was caught during adversarial review.
No files require special attention.
Important Files Changed
Reviews (2): Last reviewed commit: "address greptile review feedback (greplo..." | Re-trigger Greptile