Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 309 additions & 0 deletions execution/engine/execution_engine_cost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,315 @@ func TestExecutionEngine_Cost(t *testing.T) {
))
})

t.Run("skipImplementingTypesOnAbstract", func(t *testing.T) {
t.Run("returned abstract field should return 0 cost", runWithoutError(
ExecutionEngineTestCase{
schema: graphql.StarwarsSchema(t),
operation: func(t *testing.T) graphql.Request {
return graphql.Request{
Query: `{
hero { name }
}`,
}
},
dataSources: []plan.DataSource{
mustGraphqlDataSourceConfiguration(t, "id",
mustFactory(t,
testNetHttpClient(t, roundTripperTestCase{
expectedHost: "example.com", expectedPath: "/", expectedBody: "",
sendResponseBody: `{"data":{"hero":{"__typename":"Human","name":"Luke Skywalker"}}}`,
sendStatusCode: 200,
}),
),
&plan.DataSourceMetadata{RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{
Weights: map[plan.FieldCoordinate]*plan.FieldCost{
{TypeName: "Human", FieldName: "name"}: {HasWeight: true, Weight: 7},
{TypeName: "Droid", FieldName: "name"}: {HasWeight: true, Weight: 17},
},
Types: map[string]int{
"Human": 13,
"Droid": 11,
},
}},
customConfig,
),
},
expectedResponse: `{"data":{"hero":{"name":"Luke Skywalker"}}}`,
expectedEstimatedCost: intPtr(13), // Query.Human (13)
expectedActualCost: intPtr(13),
},
computeCosts(),
costsIgnoreImplementingTypeWeights(),
))
t.Run("hero field returning interface with concrete fragment", runWithoutError(
ExecutionEngineTestCase{
schema: graphql.StarwarsSchema(t),
operation: func(t *testing.T) graphql.Request {
return graphql.Request{
Query: `{
hero {
name
... on Human { height }
}
}`,
}
},
dataSources: []plan.DataSource{
mustGraphqlDataSourceConfiguration(t, "id",
mustFactory(t,
testNetHttpClient(t, roundTripperTestCase{
expectedHost: "example.com", expectedPath: "/", expectedBody: "",
sendResponseBody: `{"data":{"hero":{"__typename":"Human","name":"Luke Skywalker","height":"12"}}}`,
sendStatusCode: 200,
}),
),
&plan.DataSourceMetadata{RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{
Weights: map[plan.FieldCoordinate]*plan.FieldCost{
{TypeName: "Query", FieldName: "hero"}: {HasWeight: true, Weight: 2},
{TypeName: "Human", FieldName: "height"}: {HasWeight: true, Weight: 3},
{TypeName: "Human", FieldName: "name"}: {HasWeight: true, Weight: 7},
{TypeName: "Droid", FieldName: "name"}: {HasWeight: true, Weight: 17},
},
Types: map[string]int{
"Human": 13,
},
}},
customConfig,
),
},
expectedResponse: `{"data":{"hero":{"name":"Luke Skywalker","height":"12"}}}`,
expectedEstimatedCost: intPtr(5), // Query.hero (2) + Human.height (3)
expectedActualCost: intPtr(5),
},
computeCosts(),
costsIgnoreImplementingTypeWeights(),
))
t.Run("query hero with assumedSize on friends", runWithoutError(
ExecutionEngineTestCase{
schema: graphql.StarwarsSchema(t),
operation: func(t *testing.T) graphql.Request {
return graphql.Request{
Query: `{
hero {
friends {
...on Droid { name primaryFunction }
...on Human { name height }
}
}
}`,
}
},
dataSources: []plan.DataSource{
mustGraphqlDataSourceConfiguration(t, "id",
mustFactory(t,
testNetHttpClient(t, roundTripperTestCase{
expectedHost: "example.com", expectedPath: "/", expectedBody: "",
sendResponseBody: `{"data":{"hero":{"__typename":"Human","friends":[
{"__typename":"Human","name":"Luke Skywalker","height":"12"},
{"__typename":"Droid","name":"R2DO","primaryFunction":"joke"}
]}}}`,
sendStatusCode: 200,
}),
),
&plan.DataSourceMetadata{
RootNodes: rootNodes,
ChildNodes: childNodes,
CostConfig: &plan.DataSourceCostConfig{
Weights: map[plan.FieldCoordinate]*plan.FieldCost{
{TypeName: "Human", FieldName: "height"}: {HasWeight: true, Weight: 1},
{TypeName: "Human", FieldName: "name"}: {HasWeight: true, Weight: 2},
{TypeName: "Droid", FieldName: "name"}: {HasWeight: true, Weight: 2},
},
ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{
{TypeName: "Human", FieldName: "friends"}: {AssumedSize: 5},
{TypeName: "Droid", FieldName: "friends"}: {AssumedSize: 20},
},
Types: map[string]int{
"Human": 7,
"Droid": 5,
},
},
},
customConfig,
),
},
expectedResponse: `{"data":{"hero":{"friends":[{"name":"Luke Skywalker","height":"12"},{"name":"R2DO","primaryFunction":"joke"}]}}}`,
expectedEstimatedCost: intPtr(107), // max(7,5)+ 10 * (7 + max(2,2+1))
expectedActualCost: intPtr(24), // hero(7) + 2 * (6 + 0.5*(2+0+2+1))
},
computeCosts(),
costsIgnoreImplementingTypeWeights(),
))
t.Run("query hero with assumedSize on friends and weight defined", runWithoutError(
ExecutionEngineTestCase{
schema: graphql.StarwarsSchema(t),
operation: func(t *testing.T) graphql.Request {
return graphql.Request{
Query: `{
hero {
friends {
...on Droid { name primaryFunction }
...on Human { name height }
}
}
}`,
}
},
dataSources: []plan.DataSource{
mustGraphqlDataSourceConfiguration(t, "id",
mustFactory(t,
testNetHttpClient(t, roundTripperTestCase{
expectedHost: "example.com", expectedPath: "/", expectedBody: "",
sendResponseBody: `{"data":{"hero":{"__typename":"Human","friends":[
{"__typename":"Human","name":"Luke Skywalker","height":"12"},
{"__typename":"Droid","name":"R2DO","primaryFunction":"joke"}
]}}}`,
sendStatusCode: 200,
}),
),
&plan.DataSourceMetadata{
RootNodes: rootNodes,
ChildNodes: childNodes,
CostConfig: &plan.DataSourceCostConfig{
Weights: map[plan.FieldCoordinate]*plan.FieldCost{
{TypeName: "Human", FieldName: "friends"}: {HasWeight: true, Weight: 3},
{TypeName: "Droid", FieldName: "friends"}: {HasWeight: true, Weight: 4},
{TypeName: "Human", FieldName: "height"}: {HasWeight: true, Weight: 1},
{TypeName: "Human", FieldName: "name"}: {HasWeight: true, Weight: 2},
{TypeName: "Droid", FieldName: "name"}: {HasWeight: true, Weight: 2},
},
ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{
{TypeName: "Human", FieldName: "friends"}: {AssumedSize: 5},
{TypeName: "Droid", FieldName: "friends"}: {AssumedSize: 20},
},
Types: map[string]int{
"Human": 7,
"Droid": 5,
},
},
},
customConfig,
),
},
expectedResponse: `{"data":{"hero":{"friends":[{"name":"Luke Skywalker","height":"12"},{"name":"R2DO","primaryFunction":"joke"}]}}}`,
expectedEstimatedCost: intPtr(107), // hero(max(7,5))+ 10 * (7+max(2, 2+1))
expectedActualCost: intPtr(24), // hero(7) + 2 * (6+0.5*(2+2+1))
},
computeCosts(),
costsIgnoreImplementingTypeWeights(),
))
t.Run("named fragment on interface without typenames on friends", runWithoutError(
ExecutionEngineTestCase{
schema: graphql.StarwarsSchema(t),
operation: func(t *testing.T) graphql.Request {
return graphql.Request{
Query: `
fragment CharacterFields on Character {
name
friends { name }
}
{ hero { ...CharacterFields } }
`,
}
},
dataSources: []plan.DataSource{
mustGraphqlDataSourceConfiguration(t, "id",
mustFactory(t,
testNetHttpClient(t, roundTripperTestCase{
expectedHost: "example.com",
expectedPath: "/",
expectedBody: "",
// Is it possible that friends items would be returned without __typename?
sendResponseBody: `{"data":{"hero":{"__typename":"Human","name":"Luke","friends":[{"name":"Leia"}]}}}`,
sendStatusCode: 200,
}),
),
&plan.DataSourceMetadata{
RootNodes: rootNodes,
ChildNodes: childNodes,
CostConfig: &plan.DataSourceCostConfig{
Weights: map[plan.FieldCoordinate]*plan.FieldCost{
{TypeName: "Query", FieldName: "hero"}: {HasWeight: true, Weight: 2},
{TypeName: "Human", FieldName: "name"}: {HasWeight: true, Weight: 3},
{TypeName: "Droid", FieldName: "name"}: {HasWeight: true, Weight: 5},
},
ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{
{TypeName: "Human", FieldName: "friends"}: {AssumedSize: 4},
{TypeName: "Droid", FieldName: "friends"}: {AssumedSize: 6},
},
Types: map[string]int{
"Human": 2,
"Droid": 3,
},
},
},
customConfig,
),
},
expectedResponse: `{"data":{"hero":{"name":"Luke","friends":[{"name":"Leia"}]}}}`,
expectedEstimatedCost: intPtr(32), // 2 + 1*(0 + 10*(3 + 1*0))
expectedActualCost: intPtr(5), // 2 + 1*(0 + 1*(3 + 1*0))
},
computeCosts(),
costsIgnoreImplementingTypeWeights(),
))

t.Run("named fragment on interface with typename on friends", runWithoutError(
ExecutionEngineTestCase{
schema: graphql.StarwarsSchema(t),
operation: func(t *testing.T) graphql.Request {
return graphql.Request{
Query: `
fragment CharacterFields on Character {
name
friends { name }
}
{ hero { ...CharacterFields } }
`,
}
},
dataSources: []plan.DataSource{
mustGraphqlDataSourceConfiguration(t, "id",
mustFactory(t,
testNetHttpClient(t, roundTripperTestCase{
expectedHost: "example.com",
expectedPath: "/",
expectedBody: "",
sendResponseBody: `{"data":{"hero":{"__typename":"Human","name":"Luke","friends":[{"__typename":"Human","name":"Leia"}]}}}`,
sendStatusCode: 200,
}),
),
&plan.DataSourceMetadata{
RootNodes: rootNodes,
ChildNodes: childNodes,
CostConfig: &plan.DataSourceCostConfig{
Weights: map[plan.FieldCoordinate]*plan.FieldCost{
{TypeName: "Query", FieldName: "hero"}: {HasWeight: true, Weight: 2},
{TypeName: "Human", FieldName: "name"}: {HasWeight: true, Weight: 3},
{TypeName: "Droid", FieldName: "name"}: {HasWeight: true, Weight: 5},
},
ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{
{TypeName: "Human", FieldName: "friends"}: {AssumedSize: 4},
{TypeName: "Droid", FieldName: "friends"}: {AssumedSize: 6},
},
Types: map[string]int{
"Human": 2,
"Droid": 3,
},
},
},
customConfig,
),
},
expectedResponse: `{"data":{"hero":{"name":"Luke","friends":[{"name":"Leia"}]}}}`,
expectedEstimatedCost: intPtr(32), // 2 + 1*(0 + 10*(3 + 1*0))
expectedActualCost: intPtr(4), // 2 + 1*(0 + 1*(2 + 1*0))
},
computeCosts(),
costsIgnoreImplementingTypeWeights(),
))
})

})

t.Run("union types", func(t *testing.T) {
Expand Down
8 changes: 8 additions & 0 deletions execution/engine/execution_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ func runExecutionTest(testCase ExecutionEngineTestCase, withError bool, expected
engineConf.plannerConfig.ValidateRequiredExternalFields = opts.validateRequiredExternalFields
engineConf.plannerConfig.ComputeCosts = opts.computeCosts
engineConf.plannerConfig.StaticCostDefaultListSize = 10
engineConf.plannerConfig.IgnoreImplementingTypeWeights = opts.ignoreImplementingTypeWeights
engineConf.plannerConfig.RelaxSubgraphOperationFieldSelectionMergingNullability = opts.relaxFieldSelectionMergingNullability
resolveOpts := resolve.ResolverOptions{
MaxConcurrency: 1024,
Expand Down Expand Up @@ -347,6 +348,7 @@ type _executionTestOptions struct {
propagateFetchReasons bool
validateRequiredExternalFields bool
computeCosts bool
ignoreImplementingTypeWeights bool
relaxFieldSelectionMergingNullability bool
}

Expand Down Expand Up @@ -378,6 +380,12 @@ func computeCosts() executionTestOptions {
}
}

func costsIgnoreImplementingTypeWeights() executionTestOptions {
return func(options *_executionTestOptions) {
options.ignoreImplementingTypeWeights = true
}
}

func relaxFieldSelectionMergingNullability() executionTestOptions {
return func(options *_executionTestOptions) {
options.relaxFieldSelectionMergingNullability = true
Expand Down
5 changes: 5 additions & 0 deletions v2/pkg/engine/plan/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ type Configuration struct {
// When the list size is unknown from directives, this value is used as a default for static cost.
StaticCostDefaultListSize int

// IgnoreImplementingTypeWeights, when true, ignores @cost weights contributed by
// implementing types on abstract (interface/union) fields that have no weight of their own.
// Emulates Apollo's cost behavior.
IgnoreImplementingTypeWeights bool

// RelaxSubgraphOperationFieldSelectionMergingNullability relaxes the nullability validation
// for field selection merging in upstream (subgraph) operations when enclosing types are
// non-overlapping concrete object types. This is a deliberate spec deviation.
Expand Down
Loading
Loading