Repository navigation
fix: ResolveBulk under root service lists/objects and nullable-nav keys - #539
Conversation
Root service list fields returned null on the first pass, so bulk loaders never ran and nested ResolveBulk crashed on a null BulkParameter. Let those lists participate in the two-pass flow and fall back to per-item Resolve when bulk data was not loaded. Co-authored-by: Cursor <cursoragent@cursor.com>
Root Resolve pages with nested list bulk fields (status-page shape) now participate in the two-pass flow, and Conditional bulk keys such as DetexyBoard == null ? null : DetexyBoard.SerialNumber rewrite onto the first-pass extracted field instead of leaving unbound params. Co-authored-by: Cursor <cursoragent@cursor.com>
Avoid re-invoking the root service on the second pass (lists and objects) and only use replaceInline when the replacement context is still the entity type, so Dynamic egql__* names resolve correctly under bulk fields. Co-authored-by: Cursor <cursoragent@cursor.com>
bfe10e1 to
37ed7b5
Compare
Root service lists now materialize on the first pass for ResolveBulk. UseFilter/UseSort already ran there; re-applying against the first-pass Dynamic broke with "No generic method Where/OrderBy". Nested service collections still apply on the services pass only. Co-authored-by: Cursor <cursoragent@cursor.com>
- 6.1.3 is released, so these changes are 6.1.4: new CHANGELOG section and PackageVersion bumps. 6.1.2's and 6.1.3's notes go back to what those releases shipped rather than being reworded for this change. - The bulk key selector parameter rebind was duplicated in ReplaceContext and BuildBulkKeySelection - now ExpressionUtil.RebindBulkKeySelectorParameter. - The "did this collection already exist in the first pass" element-type check was duplicated in the filter and sort extensions - now BaseFieldExtension.CollectionBuiltInFirstPass. - Drop the unrelated formatting-only changes (IsSelectedOnToSingleNode, ValidateFieldsCanMerge, the keys Concat ternary, and the object initialisers in ServiceBackedCollectionExtensionsTests) so the diff is only behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reviewed and pushed a follow-up commit (af5827a) with the administrative fixes and the two duplication nits — the behaviour changes are untouched. Suites are green on that head: core 1140 (net9 + net8), EF 47, AspNet 92. What I changed
Verification beyond your tests — two shapes I probed on your branch, both pass, so no action needed; noting them so you know they're covered:
The design question, which is the one thing still open. Every root service list/object now takes the two-pass path whether or not anything below it has a bulk resolver — an extra expression compile plus an intermediate projection for a shape that used to run in one pass. Your original PR description raised exactly this and it's still unanswered:
Two sub-questions I'd want settled before this merges:
No other blockers from me. One cosmetic thing I left alone deliberately: |
… load Measured on a root service list with no bulk resolver below it (100 rows, two scalar fields, Release): 0.088ms/query on main vs 0.158ms with the two-pass path always on - an extra expression compile and a projection of every row for a query with nothing to bulk load. Gated on a new HasBulkResolverAtOrBelow walk (the same shape as HasServicesAtOrBelow, and it looks inside fragment spreads - without that a hoisted selection quietly drops back to one call per row). Back to 0.083ms when no bulk field is selected. Which nodes materialized their own projection in the first pass is now recorded on the CompileContext, the way possibleNextContextTypes already is, so the second pass cannot disagree with the first about whether to project from that result or rebuild the service expression. Deciding it twice was the cause of the 6 failures the gate first produced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Summary
ResolveBulknested under root service-resolved lists (e.g.apiKeysfrom.Resolve<TService>(...)) so the two-pass flow loads bulk data once instead of failing with a nullBulkParameter.ResolveBulknested under root service-resolved objects that expose a list (status-page shape:statusPage { items { bulkField } }) — first pass now wraps/selects nested fields, second pass uses the materialized anon, and theDataSelectorparameter is rebound when needed.row.Board == null ? null : row.Board.SerialNumber) viaExpressionReplacer.VisitConditionalso the Conditional rewrites onto the first-pass extracted field.Test plan
ServiceRootListBulkTests+HardwareSensorStatusPageBulkTests(net8.0)lastSeen/isOfflineResolveBulk and nullable DetexyBoard key (XyAdmin Offline Active)Made with Cursor