Skip to content

fix: do not charge children of null-parents - #1574

Merged
ysmolski merged 11 commits into
masterfrom
yury/eng-9806-cost-is-charged-for-fields-even-though-they-are-not-resolved
Jul 7, 2026
Merged

fix: do not charge children of null-parents#1574
ysmolski merged 11 commits into
masterfrom
yury/eng-9806-cost-is-charged-for-fields-even-though-they-are-not-resolved

Conversation

@ysmolski

@ysmolski ysmolski commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

This fix includes basic and often used cases, but it does not include
some combinations of abstract fields and fragments.
For those I have included the test cases (disabled right now).

I have simplified how parents are used in calculations.
I have added children multiplier to distiungish them from the field cost
which is always multiplied even when null was returned on that field.

This solution can be sophisticated further if there is a need for it.
Potentially, I could take different approach of walking the response
from subgraphs and match them with Cost Tree. That would remove
many heuristics and exceptions and make things much more simple.
But that would happen in a separate PR.

typeNameStats is populated only when CC is enabled.

@ysmolski
ysmolski requested a review from a team as a code owner July 3, 2026 15:28
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 69279659-38c8-42b2-a003-69e7d7e8fa1b

📥 Commits

Reviewing files that changed from the base of the PR and between 35043b7 and 393fbb0.

📒 Files selected for processing (3)
  • execution/engine/execution_engine_cost_test.go
  • v2/pkg/engine/plan/cost.go
  • v2/pkg/engine/resolve/resolvable.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • v2/pkg/engine/resolve/resolvable.go
  • v2/pkg/engine/plan/cost.go
  • execution/engine/execution_engine_cost_test.go

📝 Walkthrough

Walkthrough

Runtime-based actual cost calculation now uses runtime type-name stats for abstract/interface billing, separates field and descendant scaling, and updates execution tests for nullable-parent and overlap cases.

Changes

Runtime-based actual cost calculation

Layer / File(s) Summary
Record type-name stats for all resolved objects
v2/pkg/engine/resolve/resolvable.go
Resolvable lazily initializes cost-control stats, records them only when enabled, and stores runtime or declared type names for non-array-item objects.
Split field and child cost scaling
v2/pkg/engine/plan/cost.go
costNodeResult tracks a separate child multiplier, total node cost splits field and descendant scaling, and abstract-field weights can be adjusted from implementing-type runtime stats.
Updated cost test expectations and null-parent coverage
execution/engine/execution_engine_test.go, execution/graphql/request.go, execution/engine/execution_engine_cost_test.go
The harness enables cost control, request cost estimation keeps the nil-calc and nil-typeStats guards, several interface cost expectations change, and a new test block covers nullable parents, partially-null lists, abstract fields, and fragment overlap scenarios.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: avoiding charges for children of null parents.
Description check ✅ Passed The description is directly related to the changeset and summarizes the null-parent cost fix and related cost-control behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yury/eng-9806-cost-is-charged-for-fields-even-though-they-are-not-resolved

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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
v2/pkg/engine/resolve/resolvable.go (1)

1036-1040: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Comment describes the wrong branch.

The comment "Fall back to the declared abstract type name when the subgraph did not return __typename" is placed inside the if typeName != nil block, but that branch is exactly when the subgraph did return __typename (overriding the default). The actual fallback happens implicitly when typeName == nil (i.e., name stays as obj.TypeName), which has no comment at all.

📝 Suggested comment fix
-	name := obj.TypeName
+	// Fall back to the declared type name when the subgraph did not return __typename.
+	name := obj.TypeName
 	if typeName != nil {
-		// Fall back to the declared abstract type name when the subgraph did not return __typename.
 		name = string(typeName)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v2/pkg/engine/resolve/resolvable.go` around lines 1036 - 1040, The inline
comment in the type-name resolution logic is attached to the wrong branch in the
resolvable handling code. Update the comment near the `name := obj.TypeName` /
`if typeName != nil` logic so it describes the actual behavior: the override
happens when `typeName` is present, and the fallback to `obj.TypeName` happens
when `typeName == nil`. Keep the fix localized to the `resolvable` type-name
selection path and align the wording with the `typeName` conditional.
execution/engine/execution_engine_cost_test.go (1)

7461-7462: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

t.Parallel() after t.Skip() is unreachable.

t.Skip calls runtime.Goexit(), so line 7462 and the entire setup below it never execute. Since this block is intentionally disabled (not implemented yet), it's harmless, but consider dropping the trailing t.Parallel() (and/or guarding the block) to avoid implying it runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@execution/engine/execution_engine_cost_test.go` around lines 7461 - 7462, The
test block in execution_engine_cost_test.go has an unreachable t.Parallel()
after t.Skip("not implemented yet"), so remove the trailing t.Parallel() from
that skipped case (or otherwise guard the block) to avoid suggesting the test
executes. Use the surrounding test function containing the skipped subtest to
locate and clean up the dead setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@v2/pkg/engine/plan/cost.go`:
- Around line 939-947: Remove the unconditional stdout debug output from the
cost hot path in CostCalculator.EstimateCost and CostCalculator.ActualCost.
Delete the fmt.Println(c.DebugPrint(...)) calls, or guard them behind an
explicit debug flag/logger so production cost calculation stays side-effect
free; keep the actual return path using c.tree.cost(input) unchanged.
- Around line 824-830: The childMultiplier logic in the cost calculation
currently skips missing typeStats entries and falls back to full charging, which
is incorrect for object fields that are always null. In the cost.go path that
updates nodeCost.childMultiplier, treat a missing input.typeStats[node.jsonPath]
entry the same as Size == 0 by assigning a 0 ratio instead of leaving the
default multiplier in place. Keep the existing behavior for nodeStats.Size > 0,
and update the branch around nodeStats/input.typeStats/nodeCost.childMultiplier
so children are not charged in full when the object never appears.

---

Nitpick comments:
In `@execution/engine/execution_engine_cost_test.go`:
- Around line 7461-7462: The test block in execution_engine_cost_test.go has an
unreachable t.Parallel() after t.Skip("not implemented yet"), so remove the
trailing t.Parallel() from that skipped case (or otherwise guard the block) to
avoid suggesting the test executes. Use the surrounding test function containing
the skipped subtest to locate and clean up the dead setup.

In `@v2/pkg/engine/resolve/resolvable.go`:
- Around line 1036-1040: The inline comment in the type-name resolution logic is
attached to the wrong branch in the resolvable handling code. Update the comment
near the `name := obj.TypeName` / `if typeName != nil` logic so it describes the
actual behavior: the override happens when `typeName` is present, and the
fallback to `obj.TypeName` happens when `typeName == nil`. Keep the fix
localized to the `resolvable` type-name selection path and align the wording
with the `typeName` conditional.
🪄 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: 907c5b54-c499-45a0-a91e-8156fc9fbdae

📥 Commits

Reviewing files that changed from the base of the PR and between 979ee23 and 40a11b2.

📒 Files selected for processing (3)
  • execution/engine/execution_engine_cost_test.go
  • v2/pkg/engine/plan/cost.go
  • v2/pkg/engine/resolve/resolvable.go

Comment thread v2/pkg/engine/plan/cost.go
Comment thread v2/pkg/engine/plan/cost.go Outdated
@ysmolski
ysmolski force-pushed the yury/eng-9806-cost-is-charged-for-fields-even-though-they-are-not-resolved branch from 423a2b0 to 9553689 Compare July 6, 2026 07:49
@ysmolski
ysmolski merged commit cf436ec into master Jul 7, 2026
10 checks passed
@ysmolski
ysmolski deleted the yury/eng-9806-cost-is-charged-for-fields-even-though-they-are-not-resolved branch July 7, 2026 08:12
ysmolski pushed a commit that referenced this pull request Jul 7, 2026
🤖 I have created a release *beep* *boop*
---


##
[2.9.1](v2.9.0...v2.9.1)
(2026-07-07)


### Bug Fixes

* do not charge children of null-parents
([#1574](#1574))
([cf436ec](cf436ec))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: wundergraph-bot[bot] <285992168+wundergraph-bot[bot]@users.noreply.github.com>
ysmolski pushed a commit that referenced this pull request Jul 7, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.17.0](execution/v1.16.0...execution/v1.17.0)
(2026-07-07)


### Features

* add defer support part 4
([#1547](#1547))
([8891a0e](8891a0e))


### Bug Fixes

* do not charge children of null-parents
([#1574](#1574))
([cf436ec](cf436ec))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: wundergraph-bot[bot] <285992168+wundergraph-bot[bot]@users.noreply.github.com>
SkArchon pushed a commit that referenced this pull request Jul 7, 2026
This fix includes basic and often used cases, but it does not include 
some combinations of abstract fields and fragments. 
For those I have included the test cases (disabled right now).

I have simplified how parents are used in calculations. 
I have added children multiplier to distiungish them from the field cost
which is always multiplied even when null was returned on that field.

This solution can be sophisticated further if there is a need for it.
Potentially, I could take different approach of walking the response
from subgraphs and match them with Cost Tree. That would remove
many heuristics and exceptions and make things much more simple.
But that would happen in a separate PR.

`typeNameStats` is populated only when CC is enabled.
SkArchon pushed a commit that referenced this pull request Jul 7, 2026
🤖 I have created a release *beep* *boop*
---


##
[2.9.1](v2.9.0...v2.9.1)
(2026-07-07)


### Bug Fixes

* do not charge children of null-parents
([#1574](#1574))
([cf436ec](cf436ec))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: wundergraph-bot[bot] <285992168+wundergraph-bot[bot]@users.noreply.github.com>
SkArchon pushed a commit that referenced this pull request Jul 7, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.17.0](execution/v1.16.0...execution/v1.17.0)
(2026-07-07)


### Features

* add defer support part 4
([#1547](#1547))
([8891a0e](8891a0e))


### Bug Fixes

* do not charge children of null-parents
([#1574](#1574))
([cf436ec](cf436ec))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: wundergraph-bot[bot] <285992168+wundergraph-bot[bot]@users.noreply.github.com>
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