feat: add flag for relaxed nullability checks on same shape#1378
feat: add flag for relaxed nullability checks on same shape#1378
Conversation
…e types When inline fragments target different concrete object types (e.g. User vs Organization), fields with the same name but different nullability (String! vs String) are safe because only one branch ever resolves for a given object. Previously the validator rejected these queries unconditionally. Add TypesAreCompatibleIgnoringNullability to ast_type.go and use it as a fallback when enclosing types cannot overlap (potentiallySameObject returns false). Strict checking is preserved when an interface is involved or the enclosing types are the same. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The nullability relaxation for field selection merging on non-overlapping concrete types is now gated behind a feature flag instead of being always active. This defaults to strict spec-compliant behavior. Consumers opt in via: - plan.Configuration.RelaxSubgraphOperationFieldSelectionMergingNullability (for upstream/subgraph operation validation) - astvalidation.WithRelaxFieldSelectionMergingNullability() (for user-facing operation validation) The plan package injects the flag into factories that implement the SubgraphFieldSelectionMergingNullabilityRelaxer interface, keeping the plan package decoupled from astvalidation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…union types Thread validation options from plannerConfig through ExecutionEngine to ValidateForSchema so the relaxed nullability flag is respected end-to-end. Add three sub-tests: a negative test proving strict validation rejects differing nullability, and two positive tests verifying correct resolution for both non-null and null email values on different union member types. Also change error comparison in runWithAndCompareError from Contains to Equal for more explicit assertions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an optional, conservative relaxation for nullability checks during GraphQL field selection merging and threads that option through AST validators, planner/factory/printKit initialization, request validation, execution engine wiring, and tests for non-overlapping-type scenarios. Strict checks remain for potentially overlapping types. Changes
Sequence Diagram(s)sequenceDiagram
participant Config
participant Factory
participant Planner
participant ExecutionEngine
participant Request
participant Validator
rect rgba(200,200,255,0.5)
Config->>Factory: set RelaxSubgraphOperationFieldSelectionMergingNullability
Factory->>Factory: getPrintKitPool(validationOptions...)
Factory->>Planner: create Planner(printKitPool)
end
rect rgba(200,255,200,0.5)
ExecutionEngine->>Planner: obtain Planner (contains printKitPool)
ExecutionEngine->>Request: ValidateForSchema(schema, validationOptions...)
Request->>Validator: DefaultOperationValidator(options...)
Validator->>Validator: FieldSelectionMerging(relaxFlag?)
Validator-->>Request: validation result
Request-->>ExecutionEngine: return result
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
execution/graphql/validation.go (1)
14-41:⚠️ Potential issue | 🟠 MajorCache key ignores validation options.
With options now affecting validation, caching only by schema hash can return a relaxed result for a strict call (or vice‑versa) when the same request is validated with different options. Consider bypassing the cache when options are provided, or incorporate an options hash into the key.
💡 Suggested fix (bypass cache when options are supplied)
func (r *Request) ValidateForSchema(schema *Schema, options ...astvalidation.Option) (result ValidationResult, err error) { if schema == nil { return ValidationResult{Valid: false, Errors: nil}, ErrNilSchema } schemaHash := schema.Hash() + useCache := len(options) == 0 if r.validForSchema == nil { r.validForSchema = map[uint64]ValidationResult{} } - if result, ok := r.validForSchema[schemaHash]; ok { - return result, nil - } + if useCache { + if result, ok := r.validForSchema[schemaHash]; ok { + return result, nil + } + } report := r.parseQueryOnce() if report.HasErrors() { return operationValidationResultFromReport(report) } validator := astvalidation.DefaultOperationValidator(options...) validator.Validate(&r.document, &schema.document, &report) result, err = operationValidationResultFromReport(report) if err != nil { return result, err } - r.validForSchema[schemaHash] = result + if useCache { + r.validForSchema[schemaHash] = result + } return result, err }
🧹 Nitpick comments (2)
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go (1)
1803-1808: Potential data race on lazy pool initialization.
getPrintKitPool()performs a non-atomic check-then-write onf.printKitPool. IfPlanner()is called concurrently when the pool is uninitialized, multiple pools could be created with one being lost.If concurrent factory usage is expected, consider using
sync.Oncefor initialization:♻️ Thread-safe lazy initialization
type Factory[T Configuration] struct { executionContext context.Context httpClient *http.Client grpcClient grpc.ClientConnInterface grpcClientProvider func() grpc.ClientConnInterface subscriptionClient GraphQLSubscriptionClient printKitPool *sync.Pool + printKitPoolOnce sync.Once + relaxNullability bool }func (f *Factory[T]) EnableSubgraphFieldSelectionMergingNullabilityRelaxation() { - f.printKitPool = newPrintKitPool(astvalidation.WithRelaxFieldSelectionMergingNullability()) + f.relaxNullability = true } func (f *Factory[T]) getPrintKitPool() *sync.Pool { - if f.printKitPool == nil { - f.printKitPool = newPrintKitPool() - } + f.printKitPoolOnce.Do(func() { + if f.relaxNullability { + f.printKitPool = newPrintKitPool(astvalidation.WithRelaxFieldSelectionMergingNullability()) + } else { + f.printKitPool = newPrintKitPool() + } + }) return f.printKitPool }v2/pkg/astvalidation/operation_validation_test.go (1)
1080-1131: Add a relaxed‑mode overlap guard test.
The new cases cover non‑overlapping concrete types, but they don’t assert thatFieldSelectionMerging(true)still rejects potentially overlapping types (e.g., interface vs object). Adding a variant will lock that behavior in.🧪 Suggested test addition
+ t.Run("still rejects differing return type nullability when overlap is possible (relaxed)", func(t *testing.T) { + runWithDefinition(t, boxDefinition, ` + { + someBox { + ... on NonNullStringBox1 { + scalar + } + ... on StringBox { + scalar + } + } + }`, FieldSelectionMerging(true), Invalid, + withValidationErrors(`fields 'scalar' conflict because they return conflicting types 'String!' and 'String'`)) + })
Use sync.Once to make the lazy initialization of printKitPool thread-safe when multiple goroutines call Planner() concurrently. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go (1)
1723-1725: Avoid reconfiguring the pool after initialization.
EnableSubgraphFieldSelectionMergingNullabilityRelaxation()replaces the pool without synchronization; if called after any planner creation, you can end up with mixed validation behavior across planners and potential data races. Consider storing a flag and applying options only at first initialization.♻️ Suggested refactor to make configuration one-time
type Factory[T Configuration] struct { executionContext context.Context httpClient *http.Client grpcClient grpc.ClientConnInterface grpcClientProvider func() grpc.ClientConnInterface subscriptionClient GraphQLSubscriptionClient printKitPool *sync.Pool printKitPoolOnce sync.Once + relaxFieldSelectionMergingNullability bool } func (f *Factory[T]) EnableSubgraphFieldSelectionMergingNullabilityRelaxation() { - f.printKitPool = newPrintKitPool(astvalidation.WithRelaxFieldSelectionMergingNullability()) + f.relaxFieldSelectionMergingNullability = true } func (f *Factory[T]) getPrintKitPool() *sync.Pool { f.printKitPoolOnce.Do(func() { - if f.printKitPool == nil { - f.printKitPool = newPrintKitPool() - } + opts := []astvalidation.Option{} + if f.relaxFieldSelectionMergingNullability { + opts = append(opts, astvalidation.WithRelaxFieldSelectionMergingNullability()) + } + f.printKitPool = newPrintKitPool(opts...) }) return f.printKitPool }Also applies to: 1796-1811
- Store relaxation as bool flag, apply in sync.Once (devsergiy review) - Bypass validation cache when custom options are provided (CodeRabbit review) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go (1)
1797-1814: Consider adding a safeguard against out-of-order calls.The
sync.Oncepattern means that ifgetPrintKitPool()(orPlanner()) is called beforeEnableSubgraphFieldSelectionMergingNullabilityRelaxation(), the relaxation flag will be ignored silently. The comment documents the requirement, but a defensive check could prevent subtle bugs.💡 Optional: Add a check or panic for out-of-order calls
func (f *Factory[T]) EnableSubgraphFieldSelectionMergingNullabilityRelaxation() { + // Ensure this is called before the pool is initialized + if f.printKitPool != nil { + panic("EnableSubgraphFieldSelectionMergingNullabilityRelaxation must be called before Planner() or getPrintKitPool()") + } f.relaxFieldMergingNullability = true }
Instead of per-factory pools, use two shared package-level pools (default + relaxed). All factories/planners share the same pool, avoiding redundant pool instances across datasources. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go`:
- Around line 1717-1721: The gci formatting linter flagged import
grouping/ordering that causes a failure near the var block (variables
defaultPrintKitPool, relaxedPrintKitPool, relaxedPrintKitPoolOnce); fix by
running your project's formatter (e.g., gci write) to auto-reorder import groups
and then run gofmt/goimports (or your repo's format command) to apply
formatting; re-run linters to confirm the import grouping issue is resolved and
commit the resulting changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go`:
- Around line 1798-1806: The getKit and releaseKit methods on Planner[T] can
panic if p.printKitPool is nil; update getKit to use p.printKitPool if non-nil
otherwise fall back to defaultPrintKitPool when calling Get(), and update
releaseKit to reset and Put on p.printKitPool if non-nil otherwise use
defaultPrintKitPool; change the methods referenced are Planner.getKit,
Planner.releaseKit (kept used by printOperation and exposed via
DataSourcePlanner.ConfigureFetch/ConfigureSubscription) so the fallback to
defaultPrintKitPool prevents nil dereference when Planner is instantiated
directly.
Fall back to defaultPrintKitPool if printKitPool is nil, preventing a panic if Planner is instantiated directly without Factory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🤖 I have created a release *beep* *boop* --- ## [1.8.0](execution/v1.7.0...execution/v1.8.0) (2026-02-16) ### Features * add flag for relaxed nullability checks on same shape ([#1378](#1378)) ([6be2e74](6be2e74)) ### Bug Fixes * enable parallel execution for federation integration tests ([#1385](#1385)) ([09d9348](09d9348)) --- 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 a flag for relaxed nullability checks * **Bug Fixes** * Enabled parallel execution for federation integration tests <!-- end of auto-generated comment: release notes by coderabbit.ai -->
🤖 I have created a release *beep* *boop* --- ## [2.0.0-rc.250](v2.0.0-rc.249...v2.0.0-rc.250) (2026-02-16) ### Features * add flag for relaxed nullability checks on same shape ([#1378](#1378)) ([6be2e74](6be2e74)) --- 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 a new flag for relaxed nullability checks on shape validation. * **Chores** * Updated version to 2.0.0-rc.250. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Summary by CodeRabbit
New Features
Documentation
Tests