Skip to content

feat: defer additional fixes#1464

Draft
devsergiy wants to merge 2 commits intofeat/eng-7770-add-defer-supportfrom
feat/eng-7770-add-defer-support-additional
Draft

feat: defer additional fixes#1464
devsergiy wants to merge 2 commits intofeat/eng-7770-add-defer-supportfrom
feat/eng-7770-add-defer-support-additional

Conversation

@devsergiy
Copy link
Copy Markdown
Member

@coderabbitai summary

Checklist

  • I have discussed my proposed changes in an issue and have received approval to proceed.
  • I have followed the coding standards of the project.
  • Tests or benchmarks have been added or updated.

Open Source AI Manifesto

This project follows the principles of the Open Source AI Manifesto. Please ensure your contribution aligns with its principles.

@devsergiy devsergiy requested a review from a team as a code owner April 8, 2026 11:56
@devsergiy devsergiy requested review from SkArchon and StarpTech and removed request for a team April 8, 2026 11:56
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 8, 2026

Important

Review skipped

Draft detected.

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 36cbe52e-56d9-46e5-8a6e-7617f68d12f2

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
📝 Walkthrough

Walkthrough

Adds docs/defer-design.md, a new design document specifying end-to-end semantics and pipeline (normalization, planning, post-processing, execution) for GraphQL @defer incremental delivery, plus data structures, wire-format examples, config flags, limitations, and code/test references.

Changes

Cohort / File(s) Summary
GraphQL Defer Design Documentation
docs/defer-design.md
New comprehensive design doc (≈484 lines) detailing @defer semantics, streamed response shape, four-phase pipeline (normalization, planning, post-processing, execution), defer ID assignment and scope rules, fetch dependency grouping, rendering/envelope rules, wire-format examples, config flags, limitations/TODOs, and code/test reference map.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant Normalizer
    participant Planner
    participant PostProcessor
    participant Executor
    participant DataSource
    Client->>Normalizer: Send query with `@defer`
    Normalizer->>Planner: Produce normalized AST with defer IDs
    Planner->>PostProcessor: Produce planned fetch trees grouped by DeferID
    PostProcessor->>Executor: Emit primary plan + deferred groups
    Executor->>DataSource: Execute primary fetches
    DataSource-->>Executor: Primary data
    Executor-->>Client: Primary response (hasNext: true/false)
    loop For each deferred group (ordered)
      Executor->>DataSource: Fetch deferred group (with DeferID)
      DataSource-->>Executor: Incremental data/chunk
      Executor-->>Client: Deferred chunk envelope (hasNext flag)
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description contains only repository templates and guidance without substantive explanation of the changes being made to the codebase. Provide a meaningful description explaining the purpose of the defer design documentation, key design decisions, and how it supports the broader defer implementation.
Title check ❓ Inconclusive The title is vague and generic, using 'additional fixes' without clarifying what specific aspect of defer functionality is being addressed. Replace with a more specific title like 'docs: add defer design specification' to clearly convey that this PR adds comprehensive design documentation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/eng-7770-add-defer-support-additional

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/defer-design.md`:
- Line 334: The markdown has heading level jumps: change the two headings
currently using "######" (the "Normal envelope (`deferItemDataNull=false`)"
heading and the similar heading in the incremental rendering subsection) to one
level higher "#####" so they sit under the existing "####" section and resolve
MD001; update both occurrences accordingly to maintain consistent heading
hierarchy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6482f06a-05fd-44f1-bef5-5c7bde984015

📥 Commits

Reviewing files that changed from the base of the PR and between 081cac6 and 980b315.

📒 Files selected for processing (1)
  • docs/defer-design.md

Comment thread docs/defer-design.md Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/defer-design.md`:
- Around line 349-357: Update the "Null envelope (`deferItemDataNull=true`)"
description to match the wire-format: state that printDeferEnvelopeNullData
emits the null item ({"data": null, "path": [...], "errors": [...]}) inside the
standard {"incremental": [ ... ]} envelope rather than omitting the outer
wrapper; change the phrase "the outer `{\"incremental\": [` wrapper are never
written" to "the item is emitted inside the standard `{\"incremental\": [ ...
]}` chunk envelope and the `{\"data\": {` opener is skipped for that item," and
apply the same wording correction to the other occurrence referenced (lines
416-420).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: efeed565-a6d7-4b62-aead-60c60b77a0f2

📥 Commits

Reviewing files that changed from the base of the PR and between 980b315 and 3232978.

📒 Files selected for processing (1)
  • docs/defer-design.md

Comment thread docs/defer-design.md
Comment on lines +349 to +357
##### Null envelope (`deferItemDataNull=true`)

`printDeferEnvelopeNullData` writes the entire item as
```json
{"data": null, "path": [...], "errors": [...]}
```

in one shot — the normal `{"data": {` opener and the outer `{"incremental": [` wrapper are never written. The walker returns immediately without descending further.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Null-envelope description contradicts the documented wire format.

The null-envelope section says the outer {"incremental":[ ... ]} wrapper is never written, but the wire-format section documents null responses with that wrapper. Please align the rendering description to avoid implementer confusion.

📝 Proposed doc fix
-`printDeferEnvelopeNullData` writes the entire item as
+`printDeferEnvelopeNullData` writes the null-data item as
 ```json
 {"data": null, "path": [...], "errors": [...]}

-in one shot — the normal {"data": { opener and the outer {"incremental": [ wrapper are never written. The walker returns immediately without descending further.
+in one shot — the normal {"data": { opener is skipped for that item. The item is then emitted inside the standard {"incremental": [ ... ]} chunk envelope. The walker returns immediately without descending further.

</details>


Also applies to: 416-420

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/defer-design.md around lines 349 - 357, Update the "Null envelope
(deferItemDataNull=true)" description to match the wire-format: state that
printDeferEnvelopeNullData emits the null item ({"data": null, "path": [...],
"errors": [...]}) inside the standard {"incremental": [ ... ]} envelope rather
than omitting the outer wrapper; change the phrase "the outer {\"incremental\": [ wrapper are never written" to "the item is emitted inside the standard
{\"incremental\": [ ... ]} chunk envelope and the {\"data\": { opener is
skipped for that item," and apply the same wording correction to the other
occurrence referenced (lines 416-420).


</details>

<!-- fingerprinting:phantom:triton:hawk:21fc2da2-0188-40e2-a9bb-a36a5dd0647a -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment thread docs/defer-design.md
Comment thread docs/defer-design.md

Resolves which datasource(s) handle each field. Also detects fields that require additional data to be fetched — `@key` fields for entity resolution and `@requires` fields for computed fields — and injects them directly into the operation AST in the correct defer scope.

**`@requires` fields** are stamped with the same `@__defer_internal` as the field that needs them. They must be present in the same deferred fetch so the field resolver has the data it depends on.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if multiple fields have overlapping requires?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, for such schemas

type User @key(fields: "id") {
id: ID!
name: String!
account: Account! @requires(fields: "billing { plan } settings { region }")
billing: Billing! @external
settings: Settings! @external
}
type Account {
type: String!
limit: Int!
}
type Billing {
plan: String! @external
}
type Settings {
region: String! @external
}
`
// Subgraph 2: owns User.billing, User.notifications.
// notifications @requires(fields: "name settings { language }") — depends on sub1 (name) and sub3 (settings).
secondSubgraphSDL := `
type User @key(fields: "id") {
id: ID!
name: String! @external
notifications: [String!]! @requires(fields: "name settings { language }")
billing: Billing!
settings: Settings! @external
}
type Billing {
plan: String!
currency: String!
}
type Settings {
language: String! @external
}
`
// Subgraph 3: owns User.settings.
thirdSubgraphSDL := `
type User @key(fields: "id") {
id: ID!
settings: Settings!
}
type Settings {
region: String!
language: String!
}

Such a query with 2 fields requires selecting a different nested field in settings, but this fields are in the same defer scope

query DeferAllFields {
user {
... @defer {
name
billing { plan }
settings { region }
account { type }
notifications
}
}
}`,

Current logic always ads an alias to the required fields in defer scope, but reuses already existing alias

So operation with added required fields will look like this

query DeferAllFields {
    user {
        name @__defer_internal(id: "1")
        billing @__defer_internal(id: "1") {
            plan @__defer_internal(id: "1")
        }
        settings @__defer_internal(id: "1") {
            region @__defer_internal(id: "1")
        }
        account @__defer_internal(id: "1") {
            type @__defer_internal(id: "1")
        }
        notifications @__defer_internal(id: "1")
        ___typename: __typename
        __internal_billing: billing @__defer_internal(id: "1") {
            plan @__defer_internal(id: "1")
        }
        __internal_settings: settings @__defer_internal(id: "1") {
            region @__defer_internal(id: "1")
            language @__defer_internal(id: "1")
        }
        __internal_name: name @__defer_internal(id: "1")
        __typename
        id
    }
}

In case of such query, when the fields with requirements in different defer scopes

query DeferDerivedFieldsOnly {
user {
name
billing { plan }
settings { region language }
... @defer { account { type } }
... @defer { notifications }
}
}`,

The query will look like this

query DeferDerivedFieldsOnly {
    user {
        name
        billing {
            plan
        }
        settings {
            region
            language
        }
        account @__defer_internal(id: "1") {
            type @__defer_internal(id: "1")
        }
        notifications @__defer_internal(id: "2")
        __internal_billing: billing @__defer_internal(id: "1") {
            plan @__defer_internal(id: "1")
        }
        __internal_settings: settings @__defer_internal(id: "1") {
            region @__defer_internal(id: "1")
        }
        __internal_name: name @__defer_internal(id: "2")
        __internal_2_settings: settings @__defer_internal(id: "2") {
            language @__defer_internal(id: "2")
        }
        __typename
        id
    }
}

We always alias requirements because in situation like this

query {
  user {
    settings {
      a
    }
    ... @defer {
      fieldRequiresSettingsB
      fieldRequiresSettingsC
    }
  }
}

We can't reuse settings, because it is not in the defer scope, and it will mean that we will fetch required fields with primary response, not via deffer group

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, after writing this comment I realized that there could be small improvement

        settings @__defer_internal(id: "1") {
            region @__defer_internal(id: "1")
        }
        __internal_settings: settings @__defer_internal(id: "1") {
            region @__defer_internal(id: "1")
            language @__defer_internal(id: "1")
        }

In this case the existing field falls into the same defer scope, so we could actually reuse it

But in the case of such an operation

        settings @__defer_internal(id: "1") {
            anyField @__defer_internal(id: "1")
            region @__defer_internal(id: "2")
        }
        __internal_settings: settings @__defer_internal(id: "1") {
            region @__defer_internal(id: "1")
            language @__defer_internal(id: "1")
        }

when needed region field is in the other defer scope, we will need to add an alias to this nested field, which currently is not implemented, as an alias is added only at the root of the requires selection set

Using aliases is a tradeoff to be able to properly distribute dependencies between fields, to assign them to a proper planner

In the last example region will be returned to the client only with defer group 2
Potentially, I could mark a field that should be returned later, but fetched earlier via the other fetch

Comment thread docs/defer-design.md
- `@defer` is removed from every fragment (inline or named spread)
- Every field inside is stamped with `@__defer_internal(id, parentDeferId, label)`
- defer IDs are assigned sequentially in AST walk order
- A `___typename` placeholder is injected into any selection set where all children are deferred
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, as I said ___ vs __ might be not the best idea. Its very hard to distiungish them.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

per discussion will change it to internal__typename

Comment thread docs/defer-design.md
Comment thread docs/defer-design.md

1. Checks `@defer(if: false)` — if disabled, removes `@defer` from the fragment but does not stamp any fields (they are treated as non-deferred).
2. Removes `@defer` from the fragment node itself.
3. Assigns a sequential integer ID to this defer group. IDs are assigned in AST walk order, so they reflect the order in which `@defer` fragments appear in the document.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So IDs on internal defers are, in fact, defer groups?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kinda

It means that a single deferID could produce multiple fetches, which are grouped in GraphQLDeferResponse by deferID, e.g. it is a separate fetch tree

Comment thread docs/defer-design.md
Comment thread docs/defer-design.md
@devsergiy devsergiy marked this pull request as draft April 9, 2026 16:58
@devsergiy devsergiy changed the base branch from feat/eng-7770-add-defer-support to master May 4, 2026 15:45
@devsergiy devsergiy changed the base branch from master to feat/eng-7770-add-defer-support May 4, 2026 15:48
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.

3 participants