Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
129 changes: 129 additions & 0 deletions execution/engine/execution_engine_cost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2596,6 +2596,135 @@ func TestExecutionEngine_Cost(t *testing.T) {
},
computeCosts(),
))

t.Run("sizedFields parent is non-list wrapper inside outer list", func(t *testing.T) {
// Regression test for ENG-9574.
// When a non-list wrapper field (Board.items_page: ItemsPage!) is configured as a
// @listSize sizedFields parent, the wrapper itself is never recorded in
// actualListSizes (population happens in walkArray only). The child list's
// averaging denominator therefore falls back to 1 instead of the number of
// wrapper occurrences, inflating the combined actual cost.
boardsSchema := `
type Query {
boards(ids: [ID!]!, limit: Int): [Board!]!
@listSize(slicingArguments: ["limit"])
@cost(weight: 10)
}
type Board @key(fields: "id") {
id: ID!
items_page(limit: Int!): ItemsPage!
@listSize(slicingArguments: ["limit"], sizedFields: ["items"])
@cost(weight: 10)
}
type ItemsPage {
items: [Item!]!
@cost(weight: 10)
}
type Item @key(fields: "id") {
id: ID!
}
`
schemaBoards, err := graphql.NewSchemaFromString(boardsSchema)
require.NoError(t, err)

boardsRootNodes := []plan.TypeField{
{TypeName: "Query", FieldNames: []string{"boards"}},
{TypeName: "Board", FieldNames: []string{"id", "items_page"}},
{TypeName: "ItemsPage", FieldNames: []string{"items"}},
{TypeName: "Item", FieldNames: []string{"id"}},
}
boardsCustomConfig := mustConfiguration(t, graphql_datasource.ConfigurationInput{
Fetch: &graphql_datasource.FetchConfiguration{
URL: "https://example.com/",
Method: "GET",
},
SchemaConfiguration: mustSchemaConfig(t, nil, boardsSchema),
})
boardsFieldConfig := []plan.FieldConfiguration{
{
TypeName: "Query", FieldName: "boards", Path: []string{"boards"},
Arguments: []plan.ArgumentConfiguration{
{Name: "ids", SourceType: plan.FieldArgumentSource, RenderConfig: plan.RenderArgumentAsGraphQLValue},
{Name: "limit", SourceType: plan.FieldArgumentSource, RenderConfig: plan.RenderArgumentAsGraphQLValue},
},
},
{
TypeName: "Board", FieldName: "items_page", Path: []string{"items_page"},
Arguments: []plan.ArgumentConfiguration{
{Name: "limit", SourceType: plan.FieldArgumentSource, RenderConfig: plan.RenderArgumentAsGraphQLValue},
},
},
}
boardsCostConfig := &plan.DataSourceCostConfig{
Weights: map[plan.FieldCoordinate]*plan.FieldCost{
{TypeName: "Query", FieldName: "boards"}: {HasWeight: true, Weight: 10},
{TypeName: "Board", FieldName: "items_page"}: {HasWeight: true, Weight: 10},
{TypeName: "ItemsPage", FieldName: "items"}: {HasWeight: true, Weight: 10},
},
ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{
{TypeName: "Query", FieldName: "boards"}: {
SlicingArguments: []string{"limit"},
},
{TypeName: "Board", FieldName: "items_page"}: {
SlicingArguments: []string{"limit"},
SizedFields: []string{"items"},
},
},
}

expectedResponse := `{"data":{"boards":[` +
`{"id":"A","items_page":{"items":[{"id":"a1"}]}},` +
`{"id":"B","items_page":{"items":[{"id":"b1"}]}},` +
`{"id":"C","items_page":{"items":[{"id":"c1"}]}},` +
`{"id":"D","items_page":{"items":[{"id":"d1"}]}}` +
`]}}`

// Correct behavior:
// parentCount should resolve to the nearest list ancestor,
// count (4 boards), giving items multiplier = 4/4 = 1.
t.Run("actual cost averages by wrapper occurrences", runWithoutError(
ExecutionEngineTestCase{
schema: schemaBoards,
operation: func(t *testing.T) graphql.Request {
return graphql.Request{
Query: `{
boards(ids: ["A","B","C","D"], limit: 4) {
id
items_page(limit: 1) {
items { id }
}
}
}`,
}
},
dataSources: []plan.DataSource{
mustGraphqlDataSourceConfiguration(t, "id",
mustFactory(t,
testNetHttpClient(t, roundTripperTestCase{
expectedHost: "example.com",
expectedPath: "/",
expectedBody: "",
sendResponseBody: expectedResponse,
sendStatusCode: 200,
}),
),
&plan.DataSourceMetadata{
RootNodes: boardsRootNodes,
ChildNodes: []plan.TypeField{},
CostConfig: boardsCostConfig,
},
boardsCustomConfig,
),
},
fields: boardsFieldConfig,
expectedResponse: expectedResponse,
// 4 * ( 10 + 1 * (10 + 1 * 10))
expectedEstimatedCost: intPtr(120),
expectedActualCost: intPtr(120),
},
computeCosts(),
))
})
})

t.Run("sizedFields on abstract types", func(t *testing.T) {
Expand Down
11 changes: 7 additions & 4 deletions v2/pkg/engine/plan/cost.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,10 +574,13 @@ func (node *CostTreeNode) costsAndMultiplier(configs map[DSHash]*DataSourceCostC
totalCount, ok := actualListSizes[node.jsonPath]
if ok && totalCount != 0 {
parentCount := 1
if lastDot := strings.LastIndex(node.jsonPath, "."); lastDot != -1 {
parentPath := node.jsonPath[:lastDot]
if pc, found := actualListSizes[parentPath]; found && pc > 0 {
parentCount = pc
// Find the list size of nearest ancestor
for p := node.parent; p != nil && p.fieldCoords != costTreeRootNodeCoords; p = p.parent {
if p.returnsListType {
if pc, found := actualListSizes[p.jsonPath]; found && pc > 0 {
parentCount = pc
}
break
}
}
// We compute average to avoid double counting for nested lists
Expand Down
4 changes: 3 additions & 1 deletion v2/pkg/engine/plan/cost_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type CostVisitor struct {
func NewCostVisitor(walker *astvisitor.Walker, operation, definition *ast.Document) *CostVisitor {
stack := make([]*CostTreeNode, 0, 16)
rootNode := CostTreeNode{
fieldCoords: FieldCoordinate{"_none", "_root"},
fieldCoords: costTreeRootNodeCoords,
}
stack = append(stack, &rootNode)
return &CostVisitor{
Expand All @@ -48,6 +48,8 @@ func NewCostVisitor(walker *astvisitor.Walker, operation, definition *ast.Docume
}
}

var costTreeRootNodeCoords = FieldCoordinate{"_none", "_root"}

// EnterField creates a partial cost node when entering a field.
// The node is filled in full in the LeaveField when fieldPlanners data is available.
func (v *CostVisitor) EnterField(fieldRef int) {
Expand Down
Loading