Skip to content

refactor(executor): introduce FieldProjectionCollector (PR-009d-i) - #1012

Open
AnthonyMDev wants to merge 1 commit into
cache-rewrite/phase-1a-cache-load-fieldsfrom
cache-rewrite/phase-1a-executor-upfront-projection
Open

refactor(executor): introduce FieldProjectionCollector (PR-009d-i)#1012
AnthonyMDev wants to merge 1 commit into
cache-rewrite/phase-1a-cache-load-fieldsfrom
cache-rewrite/phase-1a-executor-upfront-projection

Conversation

@AnthonyMDev

Copy link
Copy Markdown
Contributor

Adds the per-level selection-set traversal that drives the upfront-projection read pattern described by ADR 0007 Principle 5.

This is the d-i half of the documented PR-009d split. The ADR explicitly anticipates this split because the executor reshape is the highest-risk PR in sub-phase 1A.5; this PR is additive and unblocks the actual reshape (d-ii) by giving it a working unit-tested traversal primitive.

Stacked on PR #1009 (NormalizedCache.loadFields(_:)).

What's in this PR

  • FieldProjectionCollector.collect(selections:cacheKey:variables:resolveRuntimeType:) — walks [Selection] for one record at one level and emits Set<FieldProjection>.
  • 17 unit tests covering every Selection case + variable evaluation + dedup.

The collector is additive: no existing executor caller uses it yet. PR-009d-ii reshapes CacheDataExecutionSource to drive the collector at each level, replacing the existing DataLoader-batched loadRecords pattern with loadFields-per-level batching.

Selection-case handling

Mirrors DefaultFieldSelectionCollector.collectFields(...) so the upfront-projection path and the lazy path agree on which fields each Selection case contributes:

Case Behavior
.field Emit one projection. Selection.Field.cacheKey(with:) composes the cache field name including arguments.
.conditional(conditions, _) Enter only if conditions.evaluate(with: variables) == true.
.fragment(_) Always enter (fulfilled).
.inlineFragment(_) Enter only if resolveRuntimeType() returns an Object whose __parentType.canBeConverted(from:) is true.
.deferred(_, _, _) Always enter, regardless of @defer(if:). The cache executor sets shouldAttemptDeferredFragmentExecution = true and eagerly resolves deferred selections, so the projection set must include them.

One level at a time — by design

Object/list-of-object fields' children live at separate cache keys that aren't known until the parent's child_key_value is loaded. The collector deliberately does NOT recurse past .object or .customScalar boundaries — the caller's per-level loop is responsible for resolving the parent, discovering child cache keys, and invoking collect(...) again for each child level.

This is also why the cache layer can't take a Selection tree directly (a question that came up reviewing PR #1009): per-level loading needs the per-level decision to live in the executor, where variables + cache-key resolution + fragment dispatch live. The collector is the boundary between selection-set semantics and storage projection.

Tests

17 unit tests covering:

  • Field selections: scalar, list cardinality, object boundary stop (no recursion into nested object), argument-bearing field.
  • Conditionals: @include true/false, @skip true.
  • Fragments: regular fragment (always fulfilled), Set deduplication across outer + fragment.
  • Inline fragments: matching / non-matching / nil runtime type.
  • Deferred: no condition, if: false, if: true.
  • Edge case: empty input.

Full Apollo-UnitTestPlan: 1115 passed, 0 failed.

Visibility

@_spi(Execution) public, matching FieldProjection and NormalizedCache.loadFields(_:). The SPI bracketing across all three types stays until PR-009e/f.

References

🤖 Generated with Claude Code

@apollo-librarian

apollo-librarian Bot commented Jun 2, 2026

Copy link
Copy Markdown

✅ Docs preview ready

The preview is ready to be viewed. View the preview

File Changes

0 new, 1 changed, 0 removed
* (developer-tools)/ios/(latest)/tutorial/tutorial-define-additional-mutations.mdx

Build ID: 062dcdcdfbdec055bf3d2103
Build Logs: View logs

URL: https://www.apollographql.com/docs/deploy-preview/062dcdcdfbdec055bf3d2103


✅ AI Style Review — No Changes Detected

No MDX files were changed in this pull request.

Review Log: View detailed log

This review is AI-generated. Please use common sense when accepting these suggestions, as they may not always be accurate or appropriate for your specific context.

@AnthonyMDev
AnthonyMDev force-pushed the cache-rewrite/phase-1a-cache-load-fields branch from cd62335 to fc90f51 Compare June 2, 2026 21:02
@AnthonyMDev
AnthonyMDev force-pushed the cache-rewrite/phase-1a-executor-upfront-projection branch from d0e409a to 105ac37 Compare June 2, 2026 21:03
@AnthonyMDev
AnthonyMDev force-pushed the cache-rewrite/phase-1a-cache-load-fields branch from fc90f51 to a6ccc43 Compare June 9, 2026 18:26
@AnthonyMDev
AnthonyMDev force-pushed the cache-rewrite/phase-1a-executor-upfront-projection branch from 105ac37 to b365033 Compare June 9, 2026 18:26
@AnthonyMDev
AnthonyMDev force-pushed the cache-rewrite/phase-1a-cache-load-fields branch from a6ccc43 to f4e7692 Compare June 29, 2026 20:38
@AnthonyMDev
AnthonyMDev force-pushed the cache-rewrite/phase-1a-executor-upfront-projection branch from b365033 to fe36405 Compare June 29, 2026 20:38
Adds the per-level selection-set traversal that drives the
upfront-projection read pattern described by ADR 0007 Principle 5.
`FieldProjectionCollector.collect(...)` walks a `[Selection]` for
one record at one level and emits a `Set<FieldProjection>` ready
to be handed to `NormalizedCache.loadFields(_:)`.

This is the d-i half of the documented PR-009d split. The collector
is additive — no existing executor caller uses it yet. PR-009d-ii
reshapes `CacheDataExecutionSource` to drive the collector at each
level, replacing the existing DataLoader-batched `loadRecords`
pattern with `loadFields`-per-level batching.

## Selection-case handling

Mirrors `DefaultFieldSelectionCollector.collectFields(...)` so the
upfront-projection path and the lazy path always agree on which
fields each `Selection` case contributes:

- `.field` — emit one projection. `Selection.Field.cacheKey(with:)`
  produces the cache field name (compose with arguments per
  existing semantics).
- `.conditional(conditions, _)` — enter only if `conditions
  .evaluate(with: variables) == true`.
- `.fragment(_)` — always enter (fulfilled).
- `.inlineFragment(_)` — enter only if `resolveRuntimeType()`
  returns an `Object` whose `__parentType.canBeConverted(from:)`
  returns true.
- `.deferred(_, _, _)` — always enter, regardless of the `@defer
  (if:)` condition. The cache executor sets
  `shouldAttemptDeferredFragmentExecution = true` and eagerly
  resolves deferred selections, so the projection set must include
  them. The `if:` flag only controls whether the executor's grouping
  treats them as deferred-then-resolved or fulfilled-then-resolved;
  either path reads the fields.

## Why one level at a time

Object/list-of-object fields' children live at separate cache keys
that aren't known until the parent's `child_key_value` is loaded.
The collector deliberately does NOT recurse past `.object` or
`.customScalar` boundaries — the caller's per-level loop is
responsible for resolving the parent, discovering child cache keys,
and invoking `collect(...)` again for each child level.

## Tests

17 unit tests covering: scalar selection, list cardinality, object
column shape (`.childKey`), nested-object boundary stop, field with
arguments, `@include` true/false, `@skip` true, fragment (always
fulfilled), Set deduplication across outer+fragment, inline
fragment with matching / non-matching / nil runtime type,
`.deferred` with no condition, with `if: false`, with `if: true`,
and empty input.

Full Apollo-UnitTestPlan: 1115 passed, 0 failed.

## Status

`@_spi(Execution) public` for now, matching `FieldProjection` and
`NormalizedCache.loadFields(_:)` from PR-009b/c. The SPI bracketing
across all three types stays until the read-API shape is locked
(targeted for PR-009e/f).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AnthonyMDev
AnthonyMDev force-pushed the cache-rewrite/phase-1a-cache-load-fields branch from f4e7692 to e5bb4df Compare July 9, 2026 18:59
@AnthonyMDev
AnthonyMDev force-pushed the cache-rewrite/phase-1a-executor-upfront-projection branch from fe36405 to c7d861c Compare July 9, 2026 18:59
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