Skip to content

Conversation

@ysmolski
Copy link
Contributor

@ysmolski ysmolski commented Jan 13, 2026

First release of essential features for the static cost analysis on fields, lists.

All parameters of "cost" and "listSize" directives are supported, except for "sizedFields":

  • "cost" and "listSize" directives on fields of various types
  • variables passed as values to argumets
  • interfaces and unions, lists

Notes

Planning visitor collects information for the costCalculator via EnterField and LeaveField hooks.
Calculator builds a tree of nodes, each node corresponding to the requested field.
After the planning is done, a callee could get a ref to the calculator and request cost calculation.
Cost calculation walks the previously built tree and using variables provided with operation,
estimates the static cost.

It builds on top of IBM spec for @cost and @listsize directive with a few changes:

  • It uses Int! for weights instead of stringified floats.
  • When weight is specified for the type and a field returns the list of that type,
    this weight (along with children's costs) is multiplied too.

A few things on the TBD list:

  • Support of SizedFields of @listsize
  • Weights on fields of InputObjects with recursion
  • Weights on arguments of directives

Fixes ENG-8732

Summary by CodeRabbit

  • New Features

    • Static cost analysis: plans compute and expose static cost estimates for queries.
  • Configuration

    • Toggle to enable static-cost computation, default list-size setting, and per-source cost configurations.
  • API

    • Helpers to supply per-source cost configs, set variables for cost evaluation, and retrieve static cost on requests and plans.
  • Execution / Planning

    • Planning and execution create, attach, compute, and propagate static-cost calculators/results through the request lifecycle.
  • Tests

    • Expanded test suite and options to validate static-cost computation across many scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 13, 2026

📝 Walkthrough

Walkthrough

Adds a static-cost subsystem (visitor, cost calculator, per-data-source cost config), integrates it into planning and plans, propagates calculators and variables into execution (getCachedPlan now returns a calculator), renames visitor/AST APIs, removes internal prepare usage in execution, and extends tests to assert static-cost results.

Changes

Cohort / File(s) Summary
Execution engine & tests
execution/engine/execution_engine.go, execution/engine/execution_engine_test.go
Removed internalExecutionContext.prepare usage (callers now call setContext, setVariables, setRequest), changed getCachedPlan to return (plan.Plan, *plan.CostCalculator), wired static-cost calculator usage in Execute, and extended tests with computeStaticCost option plus new public test fields (e.g., expectedStaticCost).
Request API
execution/graphql/request.go
Request now stores static cost and exposes ComputeStaticCost(calc *plan.CostCalculator, config plan.Configuration, variables *astjson.Value) and StaticCost() int.
Static cost core
v2/pkg/engine/plan/static_cost.go
New static-cost implementation: DataSourceCostConfig, FieldWeight, FieldListSize, CostTreeNode, ArgumentInfo, CostCalculator, and logic to compute per-operation static cost (weights, list-size multipliers, argument handling).
Static cost visitor
v2/pkg/engine/plan/static_cost_visitor.go
New StaticCostVisitor that builds a hierarchical CostTreeNode during planning, records data-source hashes and argument info, and exposes the final cost tree for the planner.
Planner & plan integration
v2/pkg/engine/plan/planner.go, v2/pkg/engine/plan/plan.go
Planner gains costVisitor and registers it when Configuration.ComputeStaticCost is enabled; planner creates and populates a CostCalculator and attaches it to plans via new Plan accessors SetStaticCostCalculator / GetStaticCostCalculator. Two planner setter methods were removed.
Planner config & datasource metadata
v2/pkg/engine/plan/configuration.go, v2/pkg/engine/plan/datasource_configuration.go
Added ComputeStaticCost and StaticCostDefaultListSize to Configuration; DataSourceMetadata gains CostConfig *DataSourceCostConfig; DataSource interface adds GetCostConfig() implemented by concrete config.
Visitor / visitor API changes
v2/pkg/engine/plan/visitor.go, v2/pkg/engine/plan/node_selection_visitor.go, v2/pkg/engine/plan/path_builder_visitor.go
Visitor API renames and signature changes: operationDefinitionoperationDefinitionRef; EnterOperationDefinition(ref int)EnterOperationDefinition(opRef int); LeaveField(ref int)LeaveField(fieldRef int); AllowVisitor now permits StaticCostVisitor; nodeSelectionVisitor.visitedFieldsAbstractChecks key type changed to fieldRef.
AST utilities
v2/pkg/ast/ast_type.go
Added Document.ResolveUnderlyingType(int) and Document.ResolveListOrNameType(int) helpers to unwrap nested type wrappers.
Resolve context
v2/pkg/engine/resolve/context.go
Moved Context.RemapVariables earlier in the struct and added descriptive comments; no behavioral change.

Sequence Diagram(s)

sequenceDiagram
    participant Planner as Planner
    participant Walker as Walker
    participant StaticVisitor as StaticCostVisitor
    participant DS as DataSource
    participant CostCalc as CostCalculator
    participant Plan as Plan

    Planner->>Planner: Check Configuration.ComputeStaticCost
    alt ComputeStaticCost enabled
        Planner->>StaticVisitor: NewStaticCostVisitor(walker, operation, definition)
        Planner->>Walker: Register StaticVisitor enter/leave handlers
        loop Walk operation
            StaticVisitor->>StaticVisitor: EnterField(fieldRef)
            StaticVisitor->>StaticVisitor: LeaveField(fieldRef)
        end
        StaticVisitor->>Planner: finalCostTree()
        Planner->>CostCalc: NewCostCalculator()
        loop Per data source
            Planner->>DS: GetCostConfig()
            DS-->>Planner: DataSourceCostConfig
            Planner->>CostCalc: SetDataSourceCostConfig(dsHash, config)
        end
        Planner->>CostCalc: Set cost tree
        Planner->>Plan: SetStaticCostCalculator(costCalc)
    else
        Planner->>Plan: Build plan without static cost
    end
Loading
sequenceDiagram
    participant Execute as Execute
    participant Cache as PlanCache
    participant Plan as Plan
    participant CostCalc as CostCalculator
    participant Request as Request

    Execute->>Cache: getCachedPlan(...) -> (plan, costCalc)
    alt costCalc != nil
        Execute->>CostCalc: SetVariables(variables)
        Execute->>CostCalc: (optional) cost := CostCalc.GetStaticCost(config, variables)
        Execute->>Request: Request.ComputeStaticCost(costCalc, config, variables)
    end
    Execute->>Plan: Execute plan
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: compute static costs' clearly and concisely describes the main change: implementing static cost computation for GraphQL fields, which is the primary feature introduced across multiple files in this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


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

@ysmolski ysmolski marked this pull request as draft January 13, 2026 15:24
Copy link
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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
v2/pkg/engine/plan/datasource_configuration.go (1)

34-62: Public API break: adding GetCostConfig() to DataSource forces all external implementations to update.

The plan.DataSource interface is explicitly documented as intended for external implementation ("It's up to you to implement the DataSource interface"). This new method is a source-breaking change for any external consumers. Consider whether this interface should remain public in v2, or if breaking changes are acceptable for pre-release versions. If external stability is required, use an optional interface pattern or provide a compatible default via an embedded interface.

Also applies to: 280-296

🤖 Fix all issues with AI agents
In @execution/engine/execution_engine.go:
- Around line 71-72: The struct field formatting for lastPlan (type plan.Plan)
is misaligned causing gci/import/format lint failures; run the project's
formatting command (e.g., `gci write` or `go fmt`/project-standard formatter) to
reformat the file so the field alignment and imports conform, then commit the
updated file so the pipeline passes.

In @v2/pkg/engine/plan/planner.go:
- Around line 61-79: Plan() can panic because p.planningVisitor.costCalculator
may be nil when SetConfig() enabled ComputeStaticCost after NewPlanner(), and
its internal tree state is never cleared between Plan() calls; inside Plan()
ensure that when config.ComputeStaticCost is true you initialize
p.planningVisitor.costCalculator if nil (instantiate via NewCostCalculator and
reapply data source cost configs by iterating config.DataSources and calling
SetDataSourceCostConfig with ds.Hash()), and also reset the calculator's
accumulated state before each planning run (either call a reset method on
CostCalculator like Reset/ResetTree if available or recreate the CostCalculator
instance) so subsequent Plan() calls do not accumulate previous costs; update
Plan() to use the safe, non-nil costCalculator when calling GetTotalCost().

In @v2/pkg/engine/plan/static_cost.go:
- Around line 97-103: NewDataSourceCostConfig currently initializes Weights and
Types but leaves ListSizes nil, which can cause a panic when callers modify it;
update NewDataSourceCostConfig to also initialize ListSizes (e.g., set
ListSizes: make(map[string]int)) in the returned *DataSourceCostConfig so all
maps are ready for use when functions interact with ListSizes.

In @v2/pkg/engine/plan/visitor.go:
- Around line 408-410: The visitor method enterFieldCost (and related debug
prints in the same visitor block around lines 422-485) currently emits
fmt.Printf and unconditional logging which pollutes stdout; remove those direct
prints and unconditional logs and instead gate any diagnostic output behind the
existing debug flag or the visitor's logger (e.g., use v.debugEnabled or
v.logger.Debugf if available) so normal runs produce no stdout side effects;
update enterFieldCost and the surrounding debug statements to either drop the
prints entirely or wrap them in a debug check that uses the structured logger.
- Around line 424-485: The extractFieldArguments function must stop panicking on
PrintValueBytes errors and correctly assign literal types: replace panic(err)
with non-fatal handling (log/error return or skip the problematic argument) and
ensure argInfo.typeName is derived from the argument's kind for literal values
(switch on argValue.Kind to set "Int"/"Float"/"String"/"Boolean"/"Enum" or the
appropriate built-in names) while using v.Definition.TypeNameString only when
argValue refers to a true type reference; update any similar logic in the
sibling block around lines ~504-579, and keep argInfo.ValueBytes populated only
when PrintValueBytes succeeds so downstream list-size/AssumedSize logic receives
correct literal types and no unexpected panics occur.
🧹 Nitpick comments (10)
v2/pkg/engine/plan/configuration.go (1)

49-50: Consider adding a documentation comment for consistency.

Other configuration fields in this struct have documentation comments explaining their purpose (e.g., DisableResolveFieldPositions, EnableOperationNamePropagation). Adding a brief comment would maintain consistency and help users understand when to enable this option.

📝 Suggested documentation
 	ValidateRequiredExternalFields bool
 
+	// ComputeStaticCost enables calculation of estimated static cost for queries.
 	ComputeStaticCost bool
v2/pkg/engine/plan/datasource_configuration.go (1)

344-349: Consider returning an immutable view (or documenting mutability) for CostConfig.

GetCostConfig() returns the pointer stored in metadata. If callers may mutate it after planner creation, cost behavior becomes non-deterministic across requests. If mutability is intended, document it; otherwise consider cloning config/maps on set or on read.

execution/engine/execution_engine_test.go (2)

203-217: Make static-cost assertions explicit; current expectedStaticCost != 0 can’t validate legitimate “0 cost” cases.

Using 0 as “unset” prevents asserting a real 0-cost plan. Prefer *int / hasExpectedStaticCost bool or a sentinel like expectedStaticCostSet.

Also applies to: 219-255, 337-342


272-280: Avoid forcing verbose planner debug output for the entire test suite.

Enabling PlanningVisitor/PrintQueryPlans etc. for all tests tends to bloat logs and slow CI. Suggest defaulting these to false and enabling only for the static cost tests (or behind testing.Verbose()).

v2/pkg/engine/plan/static_cost.go (6)

116-125: Nil receiver returns 0 instead of default object weight.

ObjectTypeWeight returns 0 when receiver is nil (line 119), but non-nil receivers fall back to StaticCostDefaults.Object (which is 1). This inconsistency could cause unexpected cost differences depending on whether a nil config is passed vs an empty one.

Consider returning StaticCostDefaults.Object for consistency, or document this as intentional behavior for nil configs.


173-207: Remove debug print statements before merging.

Lines 182 and 200 contain fmt.Printf debug statements that should be removed or replaced with proper structured logging before this code is production-ready.

Proposed fix
 func (node *CostTreeNode) maxWeightImplementingField(config *DataSourceCostConfig, fieldName string) *FieldWeight {
 	var maxWeight *FieldWeight
 	for _, implTypeName := range node.implementingTypeNames {
 		// Get the cost config for the field of an implementing type.
 		coord := FieldCoordinate{implTypeName, fieldName}
 		fieldWeight := config.Weights[coord]

 		if fieldWeight != nil {
 			if fieldWeight.HasWeight && (maxWeight == nil || fieldWeight.Weight > maxWeight.Weight) {
-				fmt.Printf("found better maxWeight for %v: %v\n", coord, fieldWeight)
 				maxWeight = fieldWeight
 			}
 		}
 	}
 	return maxWeight
 }

 func (node *CostTreeNode) maxMultiplierImplementingField(config *DataSourceCostConfig, fieldName string, arguments map[string]ArgumentInfo) *FieldListSize {
 	var maxMultiplier int
 	var maxListSize *FieldListSize
 	for _, implTypeName := range node.implementingTypeNames {
 		coord := FieldCoordinate{implTypeName, fieldName}
 		listSize := config.ListSizes[coord]

 		if listSize != nil {
 			multiplier := listSize.multiplier(arguments)
 			if maxListSize == nil || multiplier > maxMultiplier {
-				fmt.Printf("found better multiplier for %v: %v\n", coord, multiplier)
 				maxMultiplier = multiplier
 				maxListSize = listSize
 			}
 		}
 	}
 	return maxListSize
 }

334-353: Silent early return may hide traversal bugs.

Line 344-346: If current.fieldRef != fieldRef, the function returns silently without calculating costs. This could mask issues where EnterField/LeaveField calls are mismatched. Consider adding a warning log or panic in debug mode to catch traversal inconsistencies.

 	current := c.stack[lastIndex]
 	if current.fieldRef != fieldRef {
+		// Consider logging: mismatched fieldRef indicates traversal bug
 		return
 	}

363-398: Remove debug print statements.

Lines 374 and 387 contain fmt.Printf debug/warning statements that should use proper structured logging or be removed before production.

Proposed fix
 	for _, dsHash := range node.dataSourceHashes {
 		dsCostConfig, ok := c.costConfigs[dsHash]
 		if !ok {
-			fmt.Printf("WARNING: no cost dsCostConfig for data source %v\n", dsHash)
 			continue
 		}

 		fieldWeight := dsCostConfig.Weights[node.fieldCoord]
 		listSize := dsCostConfig.ListSizes[node.fieldCoord]
 		// The cost directive is not allowed on fields in an interface.
 		// The cost of a field on an interface can be calculated based on the costs of
 		// the corresponding field on each concrete type implementing that interface,
 		// either directly or indirectly through other interfaces.
 		if fieldWeight != nil && node.isEnclosingTypeAbstract && parent.returnsAbstractType {
 			// Composition should not let interface fields have weights, so we assume that
 			// the enclosing type is concrete.
-			fmt.Printf("WARNING: cost directive on field %v of interface %v\n", node.fieldCoord, parent.fieldCoord)
 		}

432-439: Clarify argument type fallback logic.

The else branch (line 437) applies ObjectTypeWeight to arguments that are neither isSimple nor isInputObject. It's unclear what argument types would reach this code path. Consider adding a comment explaining valid cases, or if this is unreachable, use a panic/assertion.


465-475: Calculate() is invoked on every call to GetTree() and GetTotalCost().

Both methods call Calculate() unconditionally. If a caller invokes both methods, or calls one method multiple times, the cost tree is recalculated redundantly. Consider caching the result or adding a "calculated" flag.

Proposed improvement
// Add a calculated flag to CostTree
type CostTree struct {
	Root       *CostTreeNode
	Total      int
	calculated bool
}

func (t *CostTree) Calculate() {
	if t.calculated {
		return
	}
	if t.Root != nil {
		t.Total = t.Root.TotalCost()
	}
	t.calculated = true
}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c69bf4e and 6aec35d.

📒 Files selected for processing (11)
  • execution/engine/execution_engine.go
  • execution/engine/execution_engine_test.go
  • v2/pkg/ast/ast_type.go
  • v2/pkg/engine/plan/configuration.go
  • v2/pkg/engine/plan/datasource_configuration.go
  • v2/pkg/engine/plan/node_selection_visitor.go
  • v2/pkg/engine/plan/path_builder_visitor.go
  • v2/pkg/engine/plan/plan.go
  • v2/pkg/engine/plan/planner.go
  • v2/pkg/engine/plan/static_cost.go
  • v2/pkg/engine/plan/visitor.go
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-12-02T08:25:26.682Z
Learnt from: Noroth
Repo: wundergraph/graphql-go-tools PR: 1341
File: v2/pkg/engine/datasource/grpc_datasource/execution_plan.go:1053-1118
Timestamp: 2025-12-02T08:25:26.682Z
Learning: In v2/pkg/engine/datasource/grpc_datasource field resolver response handling for composite types (interface/union), buildFieldResolverTypeMessage correctly combines both message.Fields and message.FieldSelectionSet: message.Fields contains interface-level fields that can be selected directly on the interface (such as __typename or fields defined in the interface itself), while message.FieldSelectionSet contains type-specific fields from inline fragments. This mixing is intentional and correct for GraphQL interfaces, as interface-level fields exist outside inline fragment selections and must be handled separately from type-specific fragment selections.

Applied to files:

  • v2/pkg/engine/plan/node_selection_visitor.go
📚 Learning: 2025-11-19T10:53:06.342Z
Learnt from: Noroth
Repo: wundergraph/graphql-go-tools PR: 1341
File: v2/pkg/engine/datasource/grpc_datasource/execution_plan.go:1039-1097
Timestamp: 2025-11-19T10:53:06.342Z
Learning: In v2/pkg/engine/datasource/grpc_datasource field resolver response handling, the `resolveRequiredFields` function intentionally uses two distinct approaches: for simple GraphQL object types it populates `message.Fields`, while for composite types (interface/union) it exclusively uses `message.FieldSelectionSet` with fragment-based selections. This differs from `buildFieldMessage` (regular queries) because field resolver responses returning composite types must align with protobuf oneOf structure, where all selections—including common interface fields—are handled through fragment selections built by `buildCompositeField`. The two approaches cannot be mixed in field resolver responses.

Applied to files:

  • v2/pkg/engine/plan/node_selection_visitor.go
📚 Learning: 2025-11-19T09:42:17.644Z
Learnt from: Noroth
Repo: wundergraph/graphql-go-tools PR: 1341
File: v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor_federation.go:406-429
Timestamp: 2025-11-19T09:42:17.644Z
Learning: In the wundergraph/graphql-go-tools gRPC datasource implementation (v2/pkg/engine/datasource/grpc_datasource), field resolvers must have arguments. The system does not currently support defining field resolvers without arguments. This invariant ensures that the `parentCallID` increment in `enterFieldResolver` is always matched by a decrement in `LeaveField` (which checks `r.operation.FieldHasArguments(ref)`).

Applied to files:

  • v2/pkg/engine/plan/visitor.go
🧬 Code graph analysis (5)
v2/pkg/engine/plan/datasource_configuration.go (1)
v2/pkg/engine/plan/static_cost.go (1)
  • DataSourceCostConfig (74-95)
execution/engine/execution_engine.go (1)
v2/pkg/engine/plan/plan.go (1)
  • Plan (14-19)
v2/pkg/engine/plan/static_cost.go (3)
v2/pkg/engine/plan/federation_metadata.go (1)
  • FieldCoordinate (92-95)
v2/pkg/engine/plan/datasource_configuration.go (1)
  • DSHash (15-15)
v2/pkg/ast/path.go (1)
  • FieldName (23-23)
v2/pkg/engine/plan/planner.go (4)
v2/pkg/engine/plan/static_cost.go (2)
  • CostCalculator (279-288)
  • NewCostCalculator (291-306)
v2/pkg/engine/plan/visitor.go (1)
  • Visitor (33-72)
v2/pkg/astvisitor/visitor.go (1)
  • Walker (65-98)
v2/pkg/engine/plan/configuration.go (1)
  • FieldConfiguration (116-133)
execution/engine/execution_engine_test.go (2)
v2/pkg/engine/resolve/resolve.go (1)
  • ResolverOptions (92-170)
v2/pkg/engine/plan/static_cost.go (3)
  • DataSourceCostConfig (74-95)
  • FieldWeight (20-32)
  • FieldListSize (35-51)
🪛 GitHub Actions: execution
execution/engine/execution_engine.go

[error] 72-72: golangci-lint: File is not properly formatted (gci).

execution/engine/execution_engine_test.go

[error] 340-340: Test failed: static cost computation expected 48 but got 32 in 'multiple_slicing_arguments_as_literals'.


[error] 6002-6002: Test failure due to mismatch in static cost computation: expected 48, actual 32 in 'multiple_slicing_arguments_as_literals'.

🪛 GitHub Check: Linters (1.25)
execution/engine/execution_engine.go

[failure] 72-72:
File is not properly formatted (gci)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CI Success
🔇 Additional comments (13)
v2/pkg/engine/plan/path_builder_visitor.go (1)

46-47: LGTM!

The comment clarification improves documentation by specifying that the map key represents a fieldRef, making the code's intent clearer.

execution/engine/execution_engine.go (1)

219-219: LGTM - Test observability field assignment.

Storing the plan for test inspection is a reasonable approach. The field is unexported and documented for testing purposes only.

v2/pkg/engine/plan/node_selection_visitor.go (1)

37-37: LGTM - Comment clarifies semantic meaning.

The comment update clarifies that the int key represents a fieldRef, which improves code readability. The actual type remains map[int]struct{}.

Note: The AI-generated summary states the type changed to map[fieldRef]struct{}, but the actual code shows the type is still map[int]struct{}. The change appears to be documentation-only.

v2/pkg/ast/ast_type.go (2)

253-263: LGTM - Clean extraction of type resolution logic.

The method correctly unwraps wrapped types (NonNull, List) until reaching the underlying named type. This is already used by ResolveTypeNameBytes above, making this a good refactoring for reuse.

Minor style note: There's an extra blank line before the closing brace on line 261.


265-274: LGTM - Useful utility for list-aware type resolution.

This method provides a way to unwrap non-null wrappers while preserving list type information, which is useful for cost calculations and other type introspection scenarios.

v2/pkg/engine/plan/plan.go (2)

21-55: LGTM: simple, non-invasive storage/accessors for StaticCost.

Straightforward getters/setters on both plan types; default zero cost is reasonable when not computed.


14-19: No action needed—interface expansion is expected in v2 RC.

While adding methods to an interface is technically source-breaking in Go, v2 is in Release Candidate phase where breaking changes in minor releases are explicitly expected and documented. The Plan interface is an internal engine component with no external implementations. This concern does not require addressing before v2.0.0.

v2/pkg/engine/plan/static_cost.go (6)

5-17: LGTM!

The default cost values are sensible and well-documented. The structure provides clear separation of concerns for different GraphQL element types.


19-32: LGTM!

The HasWeight flag pattern effectively distinguishes between "weight explicitly set to 0" and "no weight configured", which is important for correct cost calculation semantics.


53-70: LGTM!

The multiplier calculation logic correctly implements the fallback chain: slicing argument values → AssumedSize → global default. The use of -1 as a sentinel and taking the maximum among slicing arguments is appropriate.


209-237: LGTM!

The TotalCost recursive calculation correctly handles nil nodes, applies multipliers to children costs, and floors negative argument/directive costs to zero. The documented deviation from the IBM spec (multiplying field cost by the multiplier) is reasonable and explained in the comments.


239-263: LGTM!

The ArgumentInfo struct is well-documented with clear explanations of each field's purpose, including the future use case for coordCounts with input objects.


265-276: LGTM!

The CostTree structure and Calculate method are straightforward with appropriate nil checking.

@ysmolski
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
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: 3

🤖 Fix all issues with AI agents
In `@execution/engine/execution_engine_test.go`:
- Around line 337-342: Replace the nil-check assertion on engine.lastPlan with a
fail-fast require to avoid panics: when verifying lastPlan in the test
(referencing engine.lastPlan and local variable lastPlan) use require.NotNil(t,
lastPlan) instead of assert.NotNil so the test stops immediately if lastPlan is
nil before calling lastPlan.GetStaticCostCalculator() and
costCalc.GetTotalCost().

In `@v2/pkg/engine/plan/plan.go`:
- Around line 26-28: GetStaticCost on SynchronousResponsePlan can panic if
StaticCostCalculator is nil; modify SynchronousResponsePlan.GetStaticCost to
guard against a nil StaticCostCalculator (e.g., if s.StaticCostCalculator == nil
return 0) before calling GetTotalCost() so it safely returns a default cost when
ComputeStaticCost is disabled.

In `@v2/pkg/engine/plan/static_cost.go`:
- Around line 204-206: Remove the stray fmt.Printf debug prints in the cost
calculation path (the statements printing "found better maxWeight for %v: %v"
and the other debug lines around where maxWeight, fieldWeight and coord are
used); either delete these fmt.Printf calls or wrap them behind an existing
debug/logger flag so they don't write to stdout in production, ensuring you use
the project's logger (or a debug conditional) consistent with other logging in
static_cost.go and leave the core logic that updates maxWeight/fieldWeight/coord
unchanged.
♻️ Duplicate comments (5)
v2/pkg/engine/plan/visitor.go (3)

466-471: Debug prints remain in production code.

The fmt.Printf statements on lines 467-468 and 471 will pollute stdout in production. This was previously flagged.


521-530: Debug print and incorrect type name derivation for literal values.

Line 521 contains a debug fmt.Printf. Additionally, lines 525 and 529 use v.Operation.TypeNameString(argValue.Ref) for literal values (Boolean, Enum, String, Float, Integer), but argValue.Ref is a value reference, not a type reference. This results in incorrect/empty type names and likely contributes to the failing test mentioned in the PR description.


553-553: Additional debug prints in argument extraction.

Lines 553 and 572-574 contain fmt.Printf statements that should be removed or gated behind a debug flag.

Also applies to: 568-575

v2/pkg/engine/plan/static_cost.go (1)

117-123: ListSizes map is not initialized in constructor.

NewDataSourceCostConfig initializes Weights and Types but omits ListSizes, which will cause a nil map panic if callers attempt to add entries.

execution/engine/execution_engine_test.go (1)

6003-6048: The slicing arguments test is now enabled and functional.

This test was previously skipped due to issues with extracting integer literal values from slicing arguments. The fix (per commit "handle slicingArguments correctly") enables proper cost calculation using the maximum of the slicing arguments (last: 12 = 12) as the list size multiplier.

🧹 Nitpick comments (2)
v2/pkg/engine/plan/static_cost.go (1)

452-453: Typo in comment: "cose" should be "cost".

Proposed fix
-// LeaveField calculates the cose of the current node and pop from the cost stack.
+// LeaveField calculates the cost of the current node and pops from the cost stack.
execution/engine/execution_engine_test.go (1)

272-280: Consider disabling verbose debug flags in committed test code.

Multiple debug flags (PrintOperationTransformations, PrintPlanningPaths, PrintQueryPlans, ConfigurationVisitor, PlanningVisitor) are enabled. This can clutter test output and slow down test execution. These appear to be development-time debugging aids that should typically be disabled before merging.

♻️ Suggested change
 engineConf.plannerConfig.Debug = plan.DebugConfiguration{
-	PrintOperationTransformations: true,
-	PrintPlanningPaths:            true,
+	PrintOperationTransformations: false,
+	PrintPlanningPaths:            false,
 	// PrintNodeSuggestions:          true,
-	PrintQueryPlans:      true,
-	ConfigurationVisitor: true,
-	PlanningVisitor:      true,
+	PrintQueryPlans:      false,
+	ConfigurationVisitor: false,
+	PlanningVisitor:      false,
 	// DatasourceVisitor:             true,
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6aec35d and 5459657.

📒 Files selected for processing (7)
  • execution/engine/execution_engine.go
  • execution/engine/execution_engine_test.go
  • v2/pkg/engine/plan/plan.go
  • v2/pkg/engine/plan/planner.go
  • v2/pkg/engine/plan/static_cost.go
  • v2/pkg/engine/plan/visitor.go
  • v2/pkg/engine/resolve/context.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • v2/pkg/engine/plan/planner.go
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-12-09T15:30:57.980Z
Learnt from: SkArchon
Repo: wundergraph/graphql-go-tools PR: 1351
File: v2/pkg/engine/resolve/context.go:160-162
Timestamp: 2025-12-09T15:30:57.980Z
Learning: Ensure all Context instances in v2/pkg/engine/resolve are created with NewContext() instead of Context{} literals. The codebase avoids defensive nil checks for Context.subgraphErrors to enforce correct usage and fail fast on misuse. Do not add defensive nil checks for map fields in Context that are initialized by NewContext().

Applied to files:

  • v2/pkg/engine/resolve/context.go
📚 Learning: 2025-11-19T09:42:17.644Z
Learnt from: Noroth
Repo: wundergraph/graphql-go-tools PR: 1341
File: v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor_federation.go:406-429
Timestamp: 2025-11-19T09:42:17.644Z
Learning: In the wundergraph/graphql-go-tools gRPC datasource implementation (v2/pkg/engine/datasource/grpc_datasource), field resolvers must have arguments. The system does not currently support defining field resolvers without arguments. This invariant ensures that the `parentCallID` increment in `enterFieldResolver` is always matched by a decrement in `LeaveField` (which checks `r.operation.FieldHasArguments(ref)`).

Applied to files:

  • v2/pkg/engine/plan/visitor.go
📚 Learning: 2025-11-17T12:47:08.376Z
Learnt from: Noroth
Repo: wundergraph/graphql-go-tools PR: 1341
File: v2/pkg/grpctest/mockservice_resolve.go:721-734
Timestamp: 2025-11-17T12:47:08.376Z
Learning: In Go, protobuf-generated getter methods (GetX()) are nil-safe: they check if the receiver is nil and return the field's default value (nil for message fields, zero values for primitives). This means chained calls like `req.GetFieldArgs().GetThreshold().GetValue()` are safe even when intermediate values are nil. Setters and clearers, however, are NOT nil-safe and will panic on nil receivers.

Applied to files:

  • v2/pkg/engine/plan/visitor.go
🧬 Code graph analysis (6)
v2/pkg/engine/resolve/context.go (2)
v2/pkg/engine/resolve/variables.go (1)
  • Variables (27-27)
v2/pkg/ast/ast_value.go (1)
  • Value (32-36)
execution/engine/execution_engine.go (6)
v2/pkg/engine/plan/plan.go (1)
  • Plan (14-18)
v2/pkg/engine/resolve/variables.go (1)
  • Variables (27-27)
v2/pkg/operationreport/operationreport.go (1)
  • Report (9-12)
v2/pkg/engine/plan/static_cost.go (1)
  • CostCalculator (407-417)
v2/pkg/pool/hash64.go (1)
  • Hash64 (10-16)
v2/pkg/engine/plan/planner.go (1)
  • NewPlanner (38-89)
v2/pkg/engine/plan/plan.go (2)
v2/pkg/engine/plan/static_cost.go (1)
  • CostCalculator (407-417)
v2/pkg/engine/resolve/response.go (2)
  • GraphQLResponse (34-42)
  • GraphQLSubscription (12-16)
v2/pkg/engine/plan/visitor.go (5)
v2/pkg/engine/plan/static_cost.go (3)
  • CostCalculator (407-417)
  • CostTreeNode (149-193)
  • ArgumentInfo (374-404)
v2/pkg/engine/plan/plan.go (3)
  • Kind (7-7)
  • SynchronousResponsePlan (20-24)
  • SynchronousResponsePlan (38-40)
v2/pkg/engine/plan/federation_metadata.go (1)
  • FieldCoordinate (92-95)
v2/pkg/engine/plan/datasource_configuration.go (1)
  • DSHash (15-15)
v2/pkg/ast/ast_value.go (7)
  • ValueKindBoolean (22-22)
  • ValueKindEnum (29-29)
  • ValueKindString (21-21)
  • ValueKindFloat (24-24)
  • ValueKindInteger (23-23)
  • ValueKindVariable (25-25)
  • ValueKindList (27-27)
execution/engine/execution_engine_test.go (5)
v2/pkg/engine/plan/configuration.go (2)
  • FieldConfigurations (105-105)
  • FieldConfiguration (116-133)
execution/engine/execution_engine.go (1)
  • NewExecutionEngine (108-144)
v2/pkg/engine/resolve/node_custom.go (1)
  • CustomResolve (7-9)
v2/pkg/engine/plan/type_field.go (1)
  • TypeField (3-8)
v2/pkg/engine/plan/static_cost.go (3)
  • DataSourceCostConfig (94-115)
  • FieldWeight (24-36)
  • FieldListSize (39-56)
v2/pkg/engine/plan/static_cost.go (2)
v2/pkg/engine/plan/federation_metadata.go (1)
  • FieldCoordinate (92-95)
v2/pkg/astvisitor/visitor.go (2)
  • EnterField (735-735)
  • LeaveField (736-736)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and test (go 1.25 / windows-latest)
  • GitHub Check: Build and test (go 1.25 / ubuntu-latest)
🔇 Additional comments (17)
v2/pkg/engine/resolve/context.go (2)

19-27: LGTM!

The field relocation and added documentation for Variables and RemapVariables improve code clarity. The comments clearly explain the purpose of each field and their relationship during resolution.


211-216: Deep copy of RemapVariables is correctly implemented.

The clone logic properly creates a new map and copies entries, ensuring the cloned context is independent of the original.

v2/pkg/engine/plan/plan.go (1)

14-17: Interface extension and implementations look correct.

The Plan interface now exposes GetStaticCostCalculator(), and both SynchronousResponsePlan and SubscriptionResponsePlan implement it correctly, returning the calculator or nil.

Also applies to: 30-32, 42-50

v2/pkg/engine/plan/visitor.go (2)

488-504: Data source hash retrieval is correct.

The getFieldDataSourceHashes function correctly maps planner IDs to data source hashes with proper bounds checking. The nil return when no planners are found is acceptable as the cost calculator handles this case.


1186-1197: Plan initialization with cost calculator looks correct.

Both SubscriptionResponsePlan and SynchronousResponsePlan are properly initialized with the costCalculator reference.

v2/pkg/engine/plan/static_cost.go (4)

58-90: multiplier method logic is sound.

The method correctly picks the maximum value among slicing arguments, falls back to AssumedSize, and finally to StaticCostDefaults.List. The variable handling with nil checks is appropriate.


231-261: Recursive cost calculation with multiplier application is correct.

The totalCost method properly:

  1. Sets costs via setCostsAndMultiplier
  2. Recursively sums children costs
  3. Floors negative argument/directive costs to zero
  4. Applies multiplier to field + children costs

419-432: NewCostCalculator correctly initializes the cost tree.

The root node with multiplier: 1 and the stack initialization are appropriate for the tree-building pattern.


454-470: LeaveField silently returns if fieldRef doesn't match.

If current.fieldRef != fieldRef, the method returns without popping the stack. This could leave the stack in an inconsistent state if there's a mismatch between EnterField and LeaveField calls.

Consider adding a warning or debug log when this mismatch occurs to aid debugging:

 	if current.fieldRef != fieldRef {
+		// This shouldn't happen in normal operation - consider logging
 		return
 	}
execution/engine/execution_engine.go (3)

65-66: lastPlan field added for test visibility.

Storing the plan after execution enables test assertions on static cost. The field is appropriately non-exported and documented.


212-219: Cost calculator variable injection is correctly placed.

Setting variables on the cost calculator after plan retrieval ensures the calculator has access to runtime variable values for accurate cost computation (e.g., for slicing arguments).


242-269: getCachedPlan correctly returns cost calculator alongside plan.

The function properly:

  1. Returns (nil, nil) on errors
  2. Retrieves the cost calculator from cached plans
  3. Attaches the cost calculator to newly created plans

The cost calculator is shared between executions using the same cached plan, but this is safe because execution is synchronous and atomic per request. SetVariables() is called once before resolution begins, and the calculator's variables field is not accessed again during that execution. The cost calculator itself is never invoked during execution (GetTotalCost() is unused in the codebase except in tests), so no race condition exists.

execution/engine/execution_engine_test.go (5)

203-217: LGTM!

The new fields in ExecutionEngineTestCase are well-structured and support the static cost feature appropriately. The skipReason pattern is a good practice for documenting why tests are skipped.


250-255: LGTM!

The computeStaticCost() option function follows the established functional options pattern used elsewhere in this file.


894-894: LGTM!

The field name correction from primaryFunctions to primaryFunction aligns with the standard Star Wars schema where a Droid has a single primary function.


5550-5570: LGTM!

The static cost computation test suite is well-structured with comprehensive coverage of various scenarios including weighted fields, interfaces, fragments, and list sizes. The inline comments documenting the expected cost calculations are helpful for maintainability.


6157-6157: LGTM!

The updated getCachedPlan calls correctly discard the new second return value using _, which is appropriate for these tests that only need to verify plan caching behavior.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@ysmolski
Copy link
Contributor Author

@coderabbitai review

Copy link
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

🤖 Fix all issues with AI agents
In `@v2/pkg/engine/plan/static_cost.go`:
- Around line 323-327: The lookup of dsCostConfig from configs[dsHash] can yield
a nil pointer (since SetDataSourceCostConfig accepts nil), so before accessing
dsCostConfig.Weights in the block that follows add a nil guard: check if
dsCostConfig == nil and if so log or warn and continue (same behavior as the
existing missing-config case). Update the code path around the configs[dsHash]
retrieval in static_cost.go (the block that currently does fmt.Printf("WARNING:
no cost dsCostConfig for data source %v\n", dsHash) and then accesses
dsCostConfig.Weights) to handle a nil dsCostConfig safely to avoid panics.
♻️ Duplicate comments (2)
v2/pkg/engine/plan/static_cost.go (1)

325-339: Avoid fmt.Printf in cost calculation paths.

These warnings will pollute stdout in production. Prefer the project logger or remove the prints.

execution/engine/execution_engine_test.go (1)

337-343: Fail fast if the static cost calculator is nil.

This avoids a potential panic if a test sets expectedStaticCost but the calculator isn’t present.

💡 Proposed tweak
 			if testCase.expectedStaticCost != 0 {
 				lastPlan := engine.lastPlan
 				require.NotNil(t, lastPlan)
 				costCalc := lastPlan.GetStaticCostCalculator()
+				require.NotNil(t, costCalc, "static cost calculator should not be nil")
 				gotCost := costCalc.GetStaticCost()
 				// fmt.Println(costCalc.DebugPrint())
 				require.Equal(t, testCase.expectedStaticCost, gotCost)
 			}
🧹 Nitpick comments (2)
v2/pkg/engine/plan/static_cost.go (2)

317-321: Reset directivesCost with other per-call fields.

This keeps the computation idempotent if directive weights are added later.

♻️ Proposed tweak
-	node.fieldCost = 0
-	node.argumentsCost = 0
-	node.multiplier = 0
+	node.fieldCost = 0
+	node.argumentsCost = 0
+	node.directivesCost = 0
+	node.multiplier = 0

495-559: Compute costs before printing in DebugPrint.

DebugPrint prints cached costs before recalculating them; if GetStaticCost() hasn’t been called, output can be stale. Consider computing once at the top and reusing the result.

♻️ Proposed tweak
 func (node *CostTreeNode) debugPrint(sb *strings.Builder, configs map[DSHash]*DataSourceCostConfig, variables *astjson.Value, depth int) {
 	// implementation is a bit crude and redundant, we could skip calculating nodes all over again.
 	// but it should suffice for debugging tests.
 	if node == nil {
 		return
 	}
+	subtreeCost := node.staticCost(configs, variables)

 	indent := strings.Repeat("    ", depth)
@@
-	subtreeCost := node.staticCost(configs, variables)
 	fmt.Fprintf(sb, "%s  cost=%d\n", indent, subtreeCost)

Copy link
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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
v2/pkg/engine/plan/configuration.go (1)

3-7: Fix gci import grouping.

Linters report a gci formatting failure; remove the extra blank line between third‑party imports (or run the formatter).

🧹 Proposed fix
 import (
 	"github.com/jensneuse/abstractlogger"
-
 	"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve"
 )
♻️ Duplicate comments (2)
execution/engine/execution_engine_test.go (1)

338-343: Guard against nil static-cost calculator.

If GetStaticCostCalculator() returns nil, GetStaticCost() will panic. Fail fast before use.

🧪 Proposed fix
 lastPlan := engine.lastPlan
 require.NotNil(t, lastPlan)
 costCalc := lastPlan.GetStaticCostCalculator()
+require.NotNil(t, costCalc, "static cost calculator should not be nil")
 gotCost := costCalc.GetStaticCost()
 require.Equal(t, testCase.expectedStaticCost, gotCost)
v2/pkg/engine/plan/static_cost.go (1)

315-324: Handle nil cost configs to avoid panic.

SetDataSourceCostConfig accepts a nil config; dereferencing it here will panic. Add a nil guard when fetching from the map.

🛡️ Proposed fix
-		dsCostConfig, ok := configs[dsHash]
-		if !ok {
-			dsCostConfig = &DataSourceCostConfig{}
-			// Save it for later use by other fields:
-			configs[dsHash] = dsCostConfig
-		}
+		dsCostConfig, ok := configs[dsHash]
+		if !ok || dsCostConfig == nil {
+			dsCostConfig = NewDataSourceCostConfig()
+			// Save it for later use by other fields:
+			configs[dsHash] = dsCostConfig
+		}

Copy link
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

🤖 Fix all issues with AI agents
In `@v2/pkg/engine/plan/configuration.go`:
- Around line 50-54: NewCostCalculator currently accepts
StaticCostDefaultListSize as-is, so when ComputeStaticCost is true a zero or
negative value will undercount list multipliers; update the constructor or the
planner/config instantiation to validate and enforce a safe default (>=1) for
StaticCostDefaultListSize: if ComputeStaticCost is true and
StaticCostDefaultListSize <= 0 then set it to 1 (or return an error), and add a
short comment documenting the invariant; reference the config field
StaticCostDefaultListSize and the NewCostCalculator function when making the
change.

Copy link
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

🤖 Fix all issues with AI agents
In `@v2/pkg/engine/plan/static_cost.go`:
- Around line 316-324: The code assumes configs[dsHash] yields a non-nil
*DataSourceCostConfig but if SetDataSourceCostConfig(dsHash, nil) was called you
can get (nil, true) and panic when reading dsCostConfig.Weights; fix by checking
whether dsCostConfig == nil after the map lookup and if so allocate a new
DataSourceCostConfig (and store it back into configs[dsHash]) before using its
fields; reference the variables/types configs, dsCostConfig,
DataSourceCostConfig and the use site where node.fieldCoord is accessed so the
nil check/initialization happens prior to reading Weights or ListSizes.
♻️ Duplicate comments (1)
v2/pkg/engine/plan/static_cost.go (1)

329-334: Add nil guard for parent before accessing parent.returnsAbstractType.

If parent is nil (e.g., at the root of the tree), accessing parent.returnsAbstractType will panic. While this may be unlikely in normal usage, a defensive check improves robustness.

Proposed fix
-		if fieldWeight != nil && node.isEnclosingTypeAbstract && parent.returnsAbstractType {
+		if fieldWeight != nil && parent != nil && node.isEnclosingTypeAbstract && parent.returnsAbstractType {
 			// Composition should not let interface fields have weights, so we assume that
 			// the enclosing type is concrete.
 			fmt.Printf("WARNING: cost directive on field %v of interface %v\n", node.fieldCoord, parent.fieldCoord)
 		}
-		if node.isEnclosingTypeAbstract && parent.returnsAbstractType {
+		if parent != nil && node.isEnclosingTypeAbstract && parent.returnsAbstractType {
🧹 Nitpick comments (2)
v2/pkg/engine/plan/static_cost.go (2)

332-332: Remove debug fmt.Printf statement.

This warning print to stdout should be removed or replaced with proper logging. Debug prints can pollute output in production.

Proposed fix
-			fmt.Printf("WARNING: cost directive on field %v of interface %v\n", node.fieldCoord, parent.fieldCoord)
+			// Note: Composition should not allow interface fields to have weights.
+			// If this code path is reached, it indicates a composition bug.

380-385: TODO acknowledged for input object argument costs.

The TODO at line 380 is tracked in the file's documented TBD list. The extra blank line before the closing brace (line 385) is a minor style nit.

Minor cleanup
 			} else if arg.isInputObject {
 				// TODO: arguments should include costs of input object fields
 			} else {
 				node.argumentsCost += dsCostConfig.ObjectTypeWeight(arg.typeName)
 			}
-
 		}

Copy link
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

🤖 Fix all issues with AI agents
In `@v2/pkg/engine/plan/static_cost.go`:
- Around line 473-480: The CostCalculator is not thread-safe: SetVariables
mutates c.variables and GetStaticCost calls c.tree.staticCost which (via
setCostsAndMultiplier) mutates node fields (fieldCost, argumentsCost,
multiplier), causing data races when the cached CostCalculator is reused
concurrently. Fix by avoiding shared mutation: either make CostCalculator
request-scoped (clone the calculator or its tree before calling
SetVariables/GetStaticCost), add synchronization around variable reads/writes
and tree mutations (e.g., a mutex on CostCalculator used in SetVariables and
GetStaticCost), or refactor staticCost/setCostsAndMultiplier to compute costs
purely functionally without mutating node fields and use c.variables immutably;
update usages of CostCalculator, SetVariables, GetStaticCost, staticCost, and
setCostsAndMultiplier accordingly.
🧹 Nitpick comments (1)
v2/pkg/engine/plan/static_cost.go (1)

482-492: DebugPrint only outputs the first root field.

For multi-root operations, children[0] drops the rest. Consider iterating all root children.

♻️ Suggested tweak
- c.tree.children[0].debugPrint(&sb, c.costConfigs, c.variables, c.defaultListSize, 0)
+ for _, child := range c.tree.children {
+ 	child.debugPrint(&sb, c.costConfigs, c.variables, c.defaultListSize, 0)
+ }

Copy link
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: 2

🤖 Fix all issues with AI agents
In `@execution/graphql/request.go`:
- Around line 197-199: The method on type Request is misnamed
ComputerStaticCost; rename it to ComputeStaticCost and update all
references/call sites to use Request.ComputeStaticCost instead of
ComputerStaticCost; keep the body unchanged (it should still call
calc.GetStaticCost(config, variables) and assign to r.staticCost) and update any
interfaces or tests that reference the old name to avoid build errors.
- Around line 9-12: The import block in request.go is misformatted causing the
CI pipeline to fail; fix it by reordering and formatting imports using the
project's formatter (e.g., run "gci write" or "gofmt -w") so the imports
"github.com/wundergraph/astjson",
"github.com/wundergraph/graphql-go-tools/v2/pkg/ast",
"github.com/wundergraph/graphql-go-tools/v2/pkg/astparser", and
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" follow the
project's import grouping/order conventions and then commit the formatted file.
♻️ Duplicate comments (1)
v2/pkg/engine/plan/static_cost_visitor.go (1)

154-206: Handle literal argument values in extractFieldArguments.

Only variables are processed; literal arguments (e.g., first: 5) currently end up with empty ArgumentInfo, which makes list-size multipliers fall back to AssumedSize. This breaks static cost for literal slicing args. Please add handling for literal kinds (ValueKindInteger, ValueKindFloat, ValueKindString, ValueKindBoolean, etc.) using the existing AST helpers for value extraction.

#!/bin/bash
# Find how literal values are extracted elsewhere
rg -n "ValueKindInteger|ValueKindFloat|ValueKindString|ValueKindBoolean" v2/pkg/engine/plan -g '*.go'
rg -n "IntValue|FloatValue|ValueToInt|ValueToString|ValueToFloat" v2/pkg/ast -g '*.go'
🧹 Nitpick comments (2)
v2/pkg/engine/plan/static_cost.go (1)

319-323: Remove or gate the debug fmt.Printf statement.

This warning print statement will pollute stdout in production. Consider removing it or gating it behind a debug flag/logger.

Proposed fix
 		if fieldWeight != nil && node.isEnclosingTypeAbstract && parent.returnsAbstractType {
 			// Composition should not let interface fields have weights, so we assume that
 			// the enclosing type is concrete.
-			fmt.Printf("WARNING: cost directive on field %v of interface %v\n", node.fieldCoord, parent.fieldCoord)
+			// This should not happen if composition is correct.
 		}
execution/engine/execution_engine_test.go (1)

338-343: Consider allowing explicit assertions for a zero static cost.

The expectedStaticCost != 0 guard skips validation when zero is the expected value. If 0 is a valid expectation, consider adding an explicit flag (e.g., hasExpectedStaticCost) or switching to a pointer.

Copy link
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: 3

🤖 Fix all issues with AI agents
In `@execution/engine/execution_engine.go`:
- Around line 216-221: The code leaves operation.staticCost stale when
costCalculator is nil; update the call site so ComputeStaticCost is invoked
unconditionally on operation (e.lastPlan = cachedPlan; then call
operation.ComputeStaticCost(...)) and modify ComputeStaticCost to internally
handle a nil costCalculator (or add a nil-safe wrapper) so it resets staticCost
when costCalculator == nil; reference operation.ComputeStaticCost,
costCalculator, e.config.plannerConfig, and execContext.resolveContext.Variables
to locate and change the logic.
- Around line 65-66: The test-only field lastPlan on ExecutionEngine is written
by Execute and raced across concurrent ServeHTTP requests; add synchronization
by introducing a mutex (e.g., lastPlanMu sync.RWMutex) on the ExecutionEngine
struct, lock it for writes around assignments to lastPlan in Execute and for
reads in a new exported getter LastPlan() that returns the plan safely, and
update tests to call engine.LastPlan() instead of accessing engine.lastPlan
directly.

In `@execution/graphql/request.go`:
- Around line 49-50: The Request struct's staticCost field can retain a stale
value when no calculator is present; update the cost-computation path (e.g., in
Request.ComputeStaticCost and any helpers that set StaticCost()/StaticCost) to
explicitly reset request.staticCost to zero (or a defined zero-state) whenever
the calculator is nil or cost calculation is skipped, and keep the nil-guard so
callers can call ComputeStaticCost unconditionally; reference Request,
staticCost, ComputeStaticCost, and StaticCost() when making the change.

@ysmolski ysmolski merged commit 004f68e into master Jan 27, 2026
12 checks passed
@ysmolski ysmolski deleted the yury/eng-8635-engine-compute-static-cost branch January 27, 2026 10:43
ysmolski pushed a commit that referenced this pull request Jan 27, 2026
🤖 I have created a release *beep* *boop*
---


##
[2.0.0-rc.246](v2.0.0-rc.245...v2.0.0-rc.246)
(2026-01-27)


### Features

* compute static costs
([#1359](#1359))
([004f68e](004f68e))

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

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Added compute static costs feature in v2

* **Chores**
  * Released v2 version 2.0.0-rc.246

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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.

4 participants