Skip to content

Commit e1baebf

Browse files
committed
Merge branch 'master' into dominik/eng-7125-allow-header-forwarding
2 parents 68b3ffd + 1dcbd3b commit e1baebf

35 files changed

+1506
-268
lines changed

execution/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## [1.8.0](https://github.com/wundergraph/graphql-go-tools/compare/execution/v1.7.0...execution/v1.8.0) (2026-02-16)
4+
5+
6+
### Features
7+
8+
* add flag for relaxed nullability checks on same shape ([#1378](https://github.com/wundergraph/graphql-go-tools/issues/1378)) ([6be2e74](https://github.com/wundergraph/graphql-go-tools/commit/6be2e74036a027f44adaff7f6bfbdbce1d1fb03b))
9+
10+
11+
### Bug Fixes
12+
13+
* enable parallel execution for federation integration tests ([#1385](https://github.com/wundergraph/graphql-go-tools/issues/1385)) ([09d9348](https://github.com/wundergraph/graphql-go-tools/commit/09d934802ed1549881123c2e805159ec81981c37))
14+
315
## [1.7.0](https://github.com/wundergraph/graphql-go-tools/compare/execution/v1.6.0...execution/v1.7.0) (2026-02-06)
416

517

execution/engine/execution_engine.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/wundergraph/graphql-go-tools/v2/pkg/ast"
1818
"github.com/wundergraph/graphql-go-tools/v2/pkg/astnormalization"
1919
"github.com/wundergraph/graphql-go-tools/v2/pkg/astprinter"
20+
"github.com/wundergraph/graphql-go-tools/v2/pkg/astvalidation"
2021
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/introspection_datasource"
2122
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan"
2223
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/postprocess"
@@ -62,6 +63,7 @@ type ExecutionEngine struct {
6263
resolver *resolve.Resolver
6364
executionPlanCache *lru.Cache
6465
apolloCompatibilityFlags apollocompatibility.Flags
66+
validationOptions []astvalidation.Option
6567
}
6668

6769
type WebsocketBeforeStartHook interface {
@@ -130,6 +132,11 @@ func NewExecutionEngine(ctx context.Context, logger abstractlogger.Logger, engin
130132
dsIDs[ds.Id()] = struct{}{}
131133
}
132134

135+
var validationOpts []astvalidation.Option
136+
if engineConfig.plannerConfig.RelaxSubgraphOperationFieldSelectionMergingNullability {
137+
validationOpts = append(validationOpts, astvalidation.WithRelaxFieldSelectionMergingNullability())
138+
}
139+
133140
return &ExecutionEngine{
134141
logger: logger,
135142
config: engineConfig,
@@ -138,6 +145,7 @@ func NewExecutionEngine(ctx context.Context, logger abstractlogger.Logger, engin
138145
apolloCompatibilityFlags: apollocompatibility.Flags{
139146
ReplaceInvalidVarError: resolverOptions.ResolvableOptions.ApolloCompatibilityReplaceInvalidVarError,
140147
},
148+
validationOptions: validationOpts,
141149
}, nil
142150
}
143151

@@ -159,7 +167,7 @@ func (e *ExecutionEngine) Execute(ctx context.Context, operation *graphql.Reques
159167
}
160168

161169
// Validate the operation against the schema.
162-
if result, err := operation.ValidateForSchema(e.config.schema); err != nil {
170+
if result, err := operation.ValidateForSchema(e.config.schema, e.validationOptions...); err != nil {
163171
return err
164172
} else if !result.Valid {
165173
return result.Errors

execution/engine/execution_engine_test.go

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ type _executionTestOptions struct {
223223
propagateFetchReasons bool
224224
validateRequiredExternalFields bool
225225
computeStaticCost bool
226+
relaxFieldSelectionMergingNullability bool
226227
}
227228

228229
type executionTestOptions func(*_executionTestOptions)
@@ -253,6 +254,12 @@ func computeStaticCost() executionTestOptions {
253254
}
254255
}
255256

257+
func relaxFieldSelectionMergingNullability() executionTestOptions {
258+
return func(options *_executionTestOptions) {
259+
options.relaxFieldSelectionMergingNullability = true
260+
}
261+
}
262+
256263
func TestExecutionEngine_Execute(t *testing.T) {
257264
run := func(testCase ExecutionEngineTestCase, withError bool, expectedErrorMessage string, options ...executionTestOptions) func(t *testing.T) {
258265
t.Helper()
@@ -289,6 +296,7 @@ func TestExecutionEngine_Execute(t *testing.T) {
289296
engineConf.plannerConfig.ValidateRequiredExternalFields = opts.validateRequiredExternalFields
290297
engineConf.plannerConfig.ComputeStaticCost = opts.computeStaticCost
291298
engineConf.plannerConfig.StaticCostDefaultListSize = 10
299+
engineConf.plannerConfig.RelaxSubgraphOperationFieldSelectionMergingNullability = opts.relaxFieldSelectionMergingNullability
292300
resolveOpts := resolve.ResolverOptions{
293301
MaxConcurrency: 1024,
294302
ResolvableOptions: opts.resolvableOptions,
@@ -321,7 +329,7 @@ func TestExecutionEngine_Execute(t *testing.T) {
321329
if withError {
322330
require.Error(t, err)
323331
if expectedErrorMessage != "" {
324-
assert.Contains(t, err.Error(), expectedErrorMessage)
332+
assert.Equal(t, expectedErrorMessage, err.Error())
325333
}
326334
} else {
327335
require.NoError(t, err)
@@ -6812,6 +6820,134 @@ func TestExecutionEngine_Execute(t *testing.T) {
68126820
})
68136821

68146822
})
6823+
6824+
t.Run("field merging with different nullability on non-overlapping union types", func(t *testing.T) {
6825+
unionSchema := `
6826+
union Entity = User | Organization
6827+
type Query { entity: Entity }
6828+
type User { id: ID!, email: String! }
6829+
type Organization { id: ID!, email: String }
6830+
`
6831+
schema, err := graphql.NewSchemaFromString(unionSchema)
6832+
require.NoError(t, err)
6833+
6834+
rootNodes := []plan.TypeField{
6835+
{TypeName: "Query", FieldNames: []string{"entity"}},
6836+
{TypeName: "User", FieldNames: []string{"id", "email"}},
6837+
{TypeName: "Organization", FieldNames: []string{"id", "email"}},
6838+
}
6839+
6840+
customConfig := mustConfiguration(t, graphql_datasource.ConfigurationInput{
6841+
Fetch: &graphql_datasource.FetchConfiguration{
6842+
URL: "https://example.com/",
6843+
Method: "POST",
6844+
},
6845+
SchemaConfiguration: mustSchemaConfig(t, nil, unionSchema),
6846+
})
6847+
6848+
fieldConfig := []plan.FieldConfiguration{
6849+
{
6850+
TypeName: "Query",
6851+
FieldName: "entity",
6852+
Path: []string{"entity"},
6853+
},
6854+
}
6855+
6856+
t.Run("without relaxation flag, validation rejects differing nullability", runWithAndCompareError(
6857+
ExecutionEngineTestCase{
6858+
schema: schema,
6859+
operation: func(t *testing.T) graphql.Request {
6860+
return graphql.Request{
6861+
OperationName: "O",
6862+
Query: `query O { entity { ... on User { email } ... on Organization { email } } }`,
6863+
}
6864+
},
6865+
dataSources: []plan.DataSource{
6866+
mustGraphqlDataSourceConfiguration(t, "ds-id",
6867+
mustFactory(t,
6868+
testNetHttpClient(t, roundTripperTestCase{
6869+
expectedHost: "example.com",
6870+
expectedPath: "/",
6871+
expectedBody: "",
6872+
sendResponseBody: `{"data":{"entity":{"__typename":"User","email":"user@test.com"}}}`,
6873+
sendStatusCode: 200,
6874+
}),
6875+
),
6876+
&plan.DataSourceMetadata{
6877+
RootNodes: rootNodes,
6878+
},
6879+
customConfig,
6880+
),
6881+
},
6882+
fields: fieldConfig,
6883+
},
6884+
`fields 'email' conflict because they return conflicting types 'String!' and 'String', locations: [], path: [query,entity,$1Organization]`,
6885+
))
6886+
6887+
t.Run("non-null email from User type", runWithoutError(
6888+
ExecutionEngineTestCase{
6889+
schema: schema,
6890+
operation: func(t *testing.T) graphql.Request {
6891+
return graphql.Request{
6892+
OperationName: "O",
6893+
Query: `query O { entity { ... on User { email } ... on Organization { email } } }`,
6894+
}
6895+
},
6896+
dataSources: []plan.DataSource{
6897+
mustGraphqlDataSourceConfiguration(t, "ds-id",
6898+
mustFactory(t,
6899+
testNetHttpClient(t, roundTripperTestCase{
6900+
expectedHost: "example.com",
6901+
expectedPath: "/",
6902+
expectedBody: "",
6903+
sendResponseBody: `{"data":{"entity":{"__typename":"User","email":"user@test.com"}}}`,
6904+
sendStatusCode: 200,
6905+
}),
6906+
),
6907+
&plan.DataSourceMetadata{
6908+
RootNodes: rootNodes,
6909+
},
6910+
customConfig,
6911+
),
6912+
},
6913+
fields: fieldConfig,
6914+
expectedResponse: `{"data":{"entity":{"email":"user@test.com"}}}`,
6915+
},
6916+
relaxFieldSelectionMergingNullability(),
6917+
))
6918+
6919+
t.Run("null email from Organization type", runWithoutError(
6920+
ExecutionEngineTestCase{
6921+
schema: schema,
6922+
operation: func(t *testing.T) graphql.Request {
6923+
return graphql.Request{
6924+
OperationName: "O",
6925+
Query: `query O { entity { ... on User { email } ... on Organization { email } } }`,
6926+
}
6927+
},
6928+
dataSources: []plan.DataSource{
6929+
mustGraphqlDataSourceConfiguration(t, "ds-id",
6930+
mustFactory(t,
6931+
testNetHttpClient(t, roundTripperTestCase{
6932+
expectedHost: "example.com",
6933+
expectedPath: "/",
6934+
expectedBody: "",
6935+
sendResponseBody: `{"data":{"entity":{"__typename":"Organization","email":null}}}`,
6936+
sendStatusCode: 200,
6937+
}),
6938+
),
6939+
&plan.DataSourceMetadata{
6940+
RootNodes: rootNodes,
6941+
},
6942+
customConfig,
6943+
),
6944+
},
6945+
fields: fieldConfig,
6946+
expectedResponse: `{"data":{"entity":{"email":null}}}`,
6947+
},
6948+
relaxFieldSelectionMergingNullability(),
6949+
))
6950+
})
68156951
}
68166952

68176953
func testNetHttpClient(t *testing.T, testCase roundTripperTestCase) *http.Client {

execution/engine/federation_integration_static_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
//go:build !race
2-
31
package engine
42

53
import (
@@ -16,14 +14,15 @@ import (
1614
"github.com/wundergraph/graphql-go-tools/v2/pkg/testing/flags"
1715
)
1816

19-
// This tests produces data races in the generated gql code. Disable it when the race
20-
// detector is enabled.
2117
func TestExecutionEngine_FederationAndSubscription_IntegrationTest(t *testing.T) {
18+
t.Parallel()
19+
2220
if flags.IsWindows {
2321
t.Skip("skip on windows - test is timing dependent")
2422
}
2523

2624
t.Run("operation", func(t *testing.T) {
25+
t.Parallel()
2726
ctx, cancelFn := context.WithCancel(context.Background())
2827
setup := federationtesting.NewFederationSetup()
2928
t.Cleanup(func() {
@@ -60,6 +59,7 @@ func TestExecutionEngine_FederationAndSubscription_IntegrationTest(t *testing.T)
6059
})
6160

6261
t.Run("subscription", func(t *testing.T) {
62+
t.Parallel()
6363
ctx, cancelFn := context.WithCancel(context.Background())
6464
setup := federationtesting.NewFederationSetup()
6565
t.Cleanup(func() {

0 commit comments

Comments
 (0)