diff --git a/execution/engine/execution_engine_cost_test.go b/execution/engine/execution_engine_cost_test.go index a82e5ad614..6e490de61a 100644 --- a/execution/engine/execution_engine_cost_test.go +++ b/execution/engine/execution_engine_cost_test.go @@ -14,9 +14,10 @@ func TestExecutionEngine_Cost(t *testing.T) { t.Run("common on star wars scheme", func(t *testing.T) { rootNodes := []plan.TypeField{ - {TypeName: "Query", FieldNames: []string{"hero", "droid"}}, + {TypeName: "Query", FieldNames: []string{"hero", "droid", "search"}}, {TypeName: "Human", FieldNames: []string{"name", "height", "friends"}}, {TypeName: "Droid", FieldNames: []string{"name", "primaryFunction", "friends"}}, + {TypeName: "Starship", FieldNames: []string{"name", "length"}}, } childNodes := []plan.TypeField{ {TypeName: "Character", FieldNames: []string{"name", "friends"}}, @@ -59,7 +60,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Droid", FieldName: "name"}: {HasWeight: true, Weight: 17}, }, }}, @@ -110,7 +111,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Query", FieldName: "droid"}: { ArgumentWeights: map[string]int{"id": 3}, HasWeight: false, @@ -165,7 +166,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Query", FieldName: "droid"}: { HasWeight: true, Weight: -10, // Negative field weight @@ -224,7 +225,7 @@ func TestExecutionEngine_Cost(t *testing.T) { }), ), &plan.DataSourceMetadata{RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + 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}, @@ -263,7 +264,7 @@ func TestExecutionEngine_Cost(t *testing.T) { }), ), &plan.DataSourceMetadata{RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Human", FieldName: "name"}: {HasWeight: true, Weight: 7}, {TypeName: "Droid", FieldName: "name"}: {HasWeight: true, Weight: 17}, }, @@ -312,7 +313,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + 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}, @@ -363,7 +364,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + 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}, @@ -423,7 +424,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + 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}, @@ -525,7 +526,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + 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}, @@ -577,7 +578,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Human", FieldName: "name"}: {HasWeight: true, Weight: 2}, {TypeName: "Droid", FieldName: "name"}: {HasWeight: true, Weight: 2}, }, @@ -630,7 +631,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + 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}, @@ -690,7 +691,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Query", FieldName: "hero"}: {HasWeight: true, Weight: 2}, {TypeName: "Human", FieldName: "name"}: {HasWeight: true, Weight: 3}, {TypeName: "Human", FieldName: "height"}: {HasWeight: true, Weight: 7}, @@ -712,6 +713,167 @@ func TestExecutionEngine_Cost(t *testing.T) { computeCosts(), )) + t.Run("cost on argument of directive", func(t *testing.T) { + t.Run("directive with default non-null argument on a field adds to cost", runWithoutError( + // search(name: String!): SearchResult @approx + ExecutionEngineTestCase{ + schema: graphql.StarwarsSchema(t), + operation: func(t *testing.T) graphql.Request { + return graphql.Request{ + Query: `{ + search(name: "Luke") { + ... on Human { name } + } + }`, + } + }, + dataSources: []plan.DataSource{ + mustGraphqlDataSourceConfiguration(t, "id", + mustFactory(t, + testNetHttpClient(t, roundTripperTestCase{ + expectedHost: "example.com", + expectedPath: "/", + expectedBody: "", + sendResponseBody: `{"data":{"search":{"__typename":"Human","name":"Luke"}}}`, + sendStatusCode: 200, + }), + ), + &plan.DataSourceMetadata{ + RootNodes: rootNodes, + ChildNodes: childNodes, + CostConfig: &plan.DataSourceCostConfig{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ + {TypeName: "Query", FieldName: "search"}: { + HasWeight: true, + Weight: 3, + ArgumentWeights: map[string]int{"name": 2}, + DirectiveArgumentWeights: map[string]int{"approx.tolerance": -5}, + }, + {TypeName: "Human", FieldName: "name"}: {HasWeight: true, Weight: 5}, + }, + }, + }, + customConfig, + ), + }, + fields: []plan.FieldConfiguration{ + { + TypeName: "Query", FieldName: "search", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "name", + SourceType: plan.FieldArgumentSource, + RenderConfig: plan.RenderArgumentAsGraphQLValue, + }, + }, + }, + }, + expectedResponse: `{"data":{"search":{"name":"Luke"}}}`, + // Query.search(3) + name arg(2) + Human.name(5) + @approx.tolerance(-5) = 5 + expectedEstimatedCost: intPtr(5), + expectedActualCost: intPtr(5), + }, + computeCosts(), + )) + + t.Run("querying interface accounts for directive costs on implementations", runWithoutError( + // type Droid implements Character { name: String! @approx } + 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: 5}, + {TypeName: "Droid", FieldName: "name"}: {DirectiveArgumentWeights: map[string]int{"approx.tolerance": -5}}, + }, + }, + }, + customConfig, + ), + }, + fields: []plan.FieldConfiguration{}, + expectedResponse: `{"data":{"hero":{"name":"Luke Skywalker"}}}`, + // Query.hero(1) + Human.name(5) + @approx.tolerance(-5) = 1 + expectedEstimatedCost: intPtr(1), + expectedActualCost: intPtr(1), + }, + computeCosts(), + )) + + t.Run("field with directive of null-value arg does not affect cost", runWithoutError( + // droid(id: ID!): Droid @approx(tolerance: null) + ExecutionEngineTestCase{ + schema: graphql.StarwarsSchema(t), + operation: func(t *testing.T) graphql.Request { + return graphql.Request{ + Query: `{ + droid(id: "R2D2") { + primaryFunction + } + }`, + } + }, + dataSources: []plan.DataSource{ + mustGraphqlDataSourceConfiguration(t, "id", + mustFactory(t, + testNetHttpClient(t, roundTripperTestCase{ + expectedHost: "example.com", expectedPath: "/", expectedBody: "", + sendResponseBody: `{"data":{"droid":{"primaryFunction":"no"}}}`, + sendStatusCode: 200, + }), + ), + &plan.DataSourceMetadata{ + RootNodes: rootNodes, + ChildNodes: childNodes, + CostConfig: &plan.DataSourceCostConfig{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ + {TypeName: "Droid", FieldName: "primaryFunction"}: {HasWeight: true, Weight: 17}, + }, + }}, + customConfig, + ), + }, + fields: []plan.FieldConfiguration{ + { + TypeName: "Query", FieldName: "droid", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "id", + SourceType: plan.FieldArgumentSource, + RenderConfig: plan.RenderArgumentAsGraphQLValue, + }, + }, + }, + }, + expectedResponse: `{"data":{"droid":{"primaryFunction":"no"}}}`, + // Query.droid (1) + droid.primaryFunction (17); @approx.tolerance is null + expectedEstimatedCost: intPtr(18), + expectedActualCost: intPtr(18), + }, + computeCosts(), + )) + }) + }) t.Run("union types", func(t *testing.T) { @@ -790,7 +952,7 @@ func TestExecutionEngine_Cost(t *testing.T) { &plan.DataSourceMetadata{ RootNodes: rootNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "User", FieldName: "name"}: {HasWeight: true, Weight: 2}, {TypeName: "User", FieldName: "email"}: {HasWeight: true, Weight: 3}, {TypeName: "Post", FieldName: "title"}: {HasWeight: true, Weight: 4}, @@ -852,7 +1014,7 @@ func TestExecutionEngine_Cost(t *testing.T) { &plan.DataSourceMetadata{ RootNodes: rootNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "User", FieldName: "name"}: {HasWeight: true, Weight: 2}, {TypeName: "Post", FieldName: "title"}: {HasWeight: true, Weight: 5}, }, @@ -948,7 +1110,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Item", FieldName: "id"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -995,7 +1157,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Item", FieldName: "id"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -1042,7 +1204,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Item", FieldName: "id"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -1088,7 +1250,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Item", FieldName: "id"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -1134,7 +1296,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Item", FieldName: "id"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -1245,7 +1407,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Comment", FieldName: "text"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -1317,7 +1479,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Comment", FieldName: "text"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -1389,7 +1551,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Comment", FieldName: "text"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -1464,7 +1626,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Comment", FieldName: "text"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -1535,7 +1697,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Comment", FieldName: "text"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -1599,7 +1761,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "User", FieldName: "id"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -1665,7 +1827,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Comment", FieldName: "text"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -1822,7 +1984,7 @@ func TestExecutionEngine_Cost(t *testing.T) { }, ChildNodes: []plan.TypeField{}, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Level5", FieldName: "value"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -2034,7 +2196,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "User", FieldName: "name"}: {HasWeight: true, Weight: 2}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -2084,7 +2246,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "User", FieldName: "name"}: {HasWeight: true, Weight: 2}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -2141,7 +2303,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "User", FieldName: "name"}: {HasWeight: true, Weight: 2}, {TypeName: "Post", FieldName: "title"}: {HasWeight: true, Weight: 3}, }, @@ -2202,7 +2364,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "User", FieldName: "name"}: {HasWeight: true, Weight: 2}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -2255,7 +2417,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "User", FieldName: "name"}: {HasWeight: true, Weight: 2}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -2305,7 +2467,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "User", FieldName: "name"}: {HasWeight: true, Weight: 2}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -2355,7 +2517,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "User", FieldName: "name"}: {HasWeight: true, Weight: 2}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -2406,7 +2568,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "User", FieldName: "name"}: {HasWeight: true, Weight: 2}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -2512,7 +2674,7 @@ func TestExecutionEngine_Cost(t *testing.T) { RootNodes: rootNodes, ChildNodes: childNodes, CostConfig: &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "User", FieldName: "name"}: {HasWeight: true, Weight: 2}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -3003,7 +3165,7 @@ func TestExecutionEngine_Cost(t *testing.T) { } costConfigWithRequireOne := &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Item", FieldName: "id"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -3023,7 +3185,7 @@ func TestExecutionEngine_Cost(t *testing.T) { } costConfigWithRequireOneDisabled := &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Item", FieldName: "id"}: {HasWeight: true, Weight: 1}, }, ListSizes: map[plan.FieldCoordinate]*plan.FieldListSize{ @@ -3604,7 +3766,7 @@ func TestExecutionEngine_Cost(t *testing.T) { SchemaConfiguration: mustSchemaConfig(t, nil, inputObjectSchema), }) costConfig := &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "CreateInput", FieldName: "name"}: {HasWeight: true, Weight: 5}, {TypeName: "CreateInput", FieldName: "email"}: {HasWeight: true, Weight: 3}, {TypeName: "CreateInput", FieldName: "age"}: {HasWeight: true, Weight: 2}, diff --git a/execution/engine/execution_engine_test.go b/execution/engine/execution_engine_test.go index 1459cbdeaf..b71aca6f8a 100644 --- a/execution/engine/execution_engine_test.go +++ b/execution/engine/execution_engine_test.go @@ -4671,7 +4671,7 @@ func TestExecutionEngine_Execute(t *testing.T) { var ds1CostConfig *plan.DataSourceCostConfig if opts.includeCostConfig { ds1CostConfig = &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "Query", FieldName: "accounts"}: {HasWeight: true, Weight: 5}, {TypeName: "User", FieldName: "some"}: {HasWeight: true, Weight: 2}, {TypeName: "Admin", FieldName: "some"}: {HasWeight: true, Weight: 3}, @@ -4686,7 +4686,7 @@ func TestExecutionEngine_Execute(t *testing.T) { var ds2CostConfig *plan.DataSourceCostConfig if opts.includeCostConfig { ds2CostConfig = &plan.DataSourceCostConfig{ - Weights: map[plan.FieldCoordinate]*plan.FieldWeight{ + Weights: map[plan.FieldCoordinate]*plan.FieldCost{ {TypeName: "User", FieldName: "name"}: {HasWeight: true, Weight: 2}, {TypeName: "User", FieldName: "title"}: {HasWeight: true, Weight: 4}, {TypeName: "Admin", FieldName: "adminName"}: {HasWeight: true, Weight: 3}, diff --git a/execution/engine/testdata/full_introspection.json b/execution/engine/testdata/full_introspection.json index 8473834888..8f7a900d9f 100644 --- a/execution/engine/testdata/full_introspection.json +++ b/execution/engine/testdata/full_introspection.json @@ -641,6 +641,25 @@ } ] }, + { + "name": "approx", + "description": "", + "locations": [ + "FIELD_DEFINITION" + ], + "args": [ + { + "name": "tolerance", + "description": "", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": "1" + } + ] + }, { "name": "include", "description": "Directs the executor to include this field or fragment only when the argument is true.", diff --git a/execution/engine/testdata/full_introspection_with_deprecated.json b/execution/engine/testdata/full_introspection_with_deprecated.json index 74f8fa552f..009012b454 100644 --- a/execution/engine/testdata/full_introspection_with_deprecated.json +++ b/execution/engine/testdata/full_introspection_with_deprecated.json @@ -675,6 +675,25 @@ } ] }, + { + "name": "approx", + "description": "", + "locations": [ + "FIELD_DEFINITION" + ], + "args": [ + { + "name": "tolerance", + "description": "", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": "1" + } + ] + }, { "name": "include", "description": "Directs the executor to include this field or fragment only when the argument is true.", diff --git a/execution/engine/testdata/full_introspection_with_typenames.json b/execution/engine/testdata/full_introspection_with_typenames.json index 2eaf5e37e9..2e7a64da46 100644 --- a/execution/engine/testdata/full_introspection_with_typenames.json +++ b/execution/engine/testdata/full_introspection_with_typenames.json @@ -725,6 +725,28 @@ } ] }, + { + "__typename": "__Directive", + "name": "approx", + "description": "", + "locations": [ + "FIELD_DEFINITION" + ], + "args": [ + { + "__typename": "__InputValue", + "name": "tolerance", + "description": "", + "type": { + "__typename": "__Type", + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": "1" + } + ] + }, { "__typename": "__Directive", "name": "include", diff --git a/v2/pkg/engine/plan/cost.go b/v2/pkg/engine/plan/cost.go index c6eca67d4d..b4e08668a6 100644 --- a/v2/pkg/engine/plan/cost.go +++ b/v2/pkg/engine/plan/cost.go @@ -14,11 +14,12 @@ https://ibm.github.io/graphql-specs/cost-spec.html It builds on top of IBM spec for @cost and @listSize directive with a few changes. -* We use Int! for weights instead of floats packed in String!. +* We use the Int! type for weights. * 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. -TODO: Weights on arguments of directives +Weights on arguments of directives are supported. If an argument is of InputObject's type, +then the weight from its fields is not counted. */ @@ -40,8 +41,8 @@ import ( const DefaultEnumScalarWeight = 0 const DefaultObjectWeight = 1 -// FieldWeight defines cost configuration for a specific field of an object or input object. -type FieldWeight struct { +// FieldCost defines cost configuration for a specific field of an object or input object. +type FieldCost struct { // Weight is the cost of this field definition. It could be negative or zero. // Should be used only if HasWeight is true. @@ -53,6 +54,10 @@ type FieldWeight struct { // ArgumentWeights maps an argument name to its weight. // Location: ARGUMENT_DEFINITION ArgumentWeights map[string]int + + // DirectiveArgumentWeights maps a directive.argument coords to its weight. + // Populated by composition from @cost on directive argument definitions. + DirectiveArgumentWeights map[string]int } // FieldListSize contains parsed data from the @listSize directive for an object field. @@ -118,7 +123,7 @@ func (ls *FieldListSize) multiplier(arguments map[string]ArgumentInfo, vars *ast type DataSourceCostConfig struct { // Weights maps field coordinate to its weights. Cannot be on fields of interfaces. // Location: FIELD_DEFINITION, INPUT_FIELD_DEFINITION - Weights map[FieldCoordinate]*FieldWeight + Weights map[FieldCoordinate]*FieldCost // ListSizes maps field coordinates to their respective list size configurations. // Location: FIELD_DEFINITION @@ -129,19 +134,12 @@ type DataSourceCostConfig struct { // Weight assigned to the field or argument definitions overrides the weight of type definition. // Location: ENUM, OBJECT, SCALAR Types map[string]int - - // Arguments on directives is a special case. They use a special kind of coordinate: - // directive name + argument name. That should be the key mapped to the weight. - // - // Directives can be used on [input] object fields and arguments of fields. This creates - // mutual recursion between them; it complicates cost calculation. - // We avoid them intentionally in the first iteration. } // NewDataSourceCostConfig creates a new cost config with defaults func NewDataSourceCostConfig() *DataSourceCostConfig { return &DataSourceCostConfig{ - Weights: make(map[FieldCoordinate]*FieldWeight), + Weights: make(map[FieldCoordinate]*FieldCost), ListSizes: make(map[FieldCoordinate]*FieldListSize), Types: make(map[string]int), } @@ -238,7 +236,7 @@ type inputObjectField struct { // inputFieldsCost computes the cost of input object fields from the variable value. // It handles both single objects and arrays of objects. -func (arg *ArgumentInfo) inputFieldsCost(variables *astjson.Value, weights map[FieldCoordinate]*FieldWeight) int { +func (arg *ArgumentInfo) inputFieldsCost(variables *astjson.Value, weights map[FieldCoordinate]*FieldCost) int { if !arg.hasVariable { return 0 } @@ -262,8 +260,8 @@ func (arg *ArgumentInfo) inputFieldsCost(variables *astjson.Value, weights map[F return 0 } -func (node *CostTreeNode) maxWeightImplementingField(config *DataSourceCostConfig, fieldName string) *FieldWeight { - var maxWeight *FieldWeight +func (node *CostTreeNode) maxWeightImplementingField(config *DataSourceCostConfig, fieldName string) *FieldCost { + var maxWeight *FieldCost for _, implTypeName := range node.implementingTypeNames { // Get the cost config for the field of an implementing type. coord := FieldCoordinate{implTypeName, fieldName} @@ -330,6 +328,29 @@ func (node *CostTreeNode) sizedFieldImplementingFields(config *DataSourceCostCon return result } +// maxDirectiveArgumentWeightsImplementingFields returns the union of DirectiveArgumentWeights +// from implementing types' field definitions. For each directive.argument pair, it takes the +// maximum weight across all implementing types. +func (node *CostTreeNode) maxDirectiveArgumentWeightsImplementingFields(config *DataSourceCostConfig, fieldName string) map[string]int { + var result map[string]int + for _, implTypeName := range node.implementingTypeNames { + coords := FieldCoordinate{implTypeName, fieldName} + fw := config.Weights[coords] + if fw == nil || len(fw.DirectiveArgumentWeights) == 0 { + continue + } + if result == nil { + result = make(map[string]int) + } + for dirArg, weight := range fw.DirectiveArgumentWeights { + if existing, ok := result[dirArg]; !ok || weight > existing { + result[dirArg] = weight + } + } + } + return result +} + // cost calculates the estimated/actual cost of this node and all descendants. // // defaultListSize designates the mode of operation. @@ -382,7 +403,7 @@ func (node *CostTreeNode) cost(configs map[DSHash]*DataSourceCostConfig, variabl // // fieldCost is the weight of this field or its returned type // argsCost is the sum of argument weights and input fields used on this field. -// Weights on directives ignored for now. +// directiveCost is the sum of directive argument weights. // // defaultListSize designates the mode of operation. // When it is positive, then its value is used as a fallback value of list sizes for the estimated cost. @@ -391,7 +412,7 @@ func (node *CostTreeNode) cost(configs map[DSHash]*DataSourceCostConfig, variabl // When estimating cost, it picks the highest multiplier among different data sources. // Also, it picks the maximum field weight of implementing types and then // the maximum among slicing arguments. -func (node *CostTreeNode) costsAndMultiplier(configs map[DSHash]*DataSourceCostConfig, variables *astjson.Value, defaultListSize int, actualListSizes map[string]int) (fieldCost, argsCost, directiveCost int, multiplier float64) { +func (node *CostTreeNode) costsAndMultiplier(configs map[DSHash]*DataSourceCostConfig, variables *astjson.Value, defaultListSize int, actualListSizes map[string]int) (fieldCost, argsCost, directivesCost int, multiplier float64) { if len(node.dataSourceHashes) <= 0 { // no data source is responsible for this field return @@ -400,7 +421,7 @@ func (node *CostTreeNode) costsAndMultiplier(configs map[DSHash]*DataSourceCostC parent := node.parent fieldCost = 0 argsCost = 0 - directiveCost = 0 + directivesCost = 0 multiplier = 0 isEstimation := defaultListSize > 0 @@ -479,6 +500,18 @@ func (node *CostTreeNode) costsAndMultiplier(configs map[DSHash]*DataSourceCostC } } + // Directive weights: sum from the field's own DirectiveArgumentWeights, + // or from implementing types when the enclosing type is abstract. + if node.isEnclosingTypeAbstract && parent.returnsAbstractType { + for _, weight := range parent.maxDirectiveArgumentWeightsImplementingFields(dsCostConfig, node.fieldCoords.FieldName) { + directivesCost += weight + } + } else if fieldWeight != nil { + for _, weight := range fieldWeight.DirectiveArgumentWeights { + directivesCost += weight + } + } + if !node.returnsListType || !isEstimation { continue } @@ -571,7 +604,7 @@ func (node *CostTreeNode) costsAndMultiplier(configs map[DSHash]*DataSourceCostC func inputObjectCost( typeName string, value *astjson.Object, - weights map[FieldCoordinate]*FieldWeight, + weights map[FieldCoordinate]*FieldCost, types map[FieldCoordinate]inputObjectField) int { if value == nil { return 0 diff --git a/v2/pkg/engine/plan/cost_visitor.go b/v2/pkg/engine/plan/cost_visitor.go index c33e4137de..cb612239c9 100644 --- a/v2/pkg/engine/plan/cost_visitor.go +++ b/v2/pkg/engine/plan/cost_visitor.go @@ -101,7 +101,7 @@ func (v *CostVisitor) EnterField(fieldRef int) { } isEnclosingTypeAbstract := v.Walker.EnclosingTypeDefinition.Kind.IsAbstractType() - // Create a skeleton node. dataSourceHashes will be filled in leaveFieldCost + // Partially filled node. dataSourceHashes will be filled in leaveFieldCost node := CostTreeNode{ fieldRef: fieldRef, fieldCoords: FieldCoordinate{typeName, fieldName}, @@ -115,7 +115,7 @@ func (v *CostVisitor) EnterField(fieldRef int) { jsonPath: jsonPath, } - // Attach to parent + // Attach to the parent if len(v.stack) > 0 { parent := v.stack[len(v.stack)-1] parent.children = append(parent.children, &node) diff --git a/v2/pkg/starwars/testdata/star_wars.graphql b/v2/pkg/starwars/testdata/star_wars.graphql index b3d78fe01c..04da682b14 100644 --- a/v2/pkg/starwars/testdata/star_wars.graphql +++ b/v2/pkg/starwars/testdata/star_wars.graphql @@ -8,10 +8,14 @@ schema { directive @testDeprecated(okArg: String deprecatedArg: String @deprecated(reason: "no such arg")) on FIELD_DEFINITION +# Used to test costs on arguments of directives: +# the tolerance argument has "@cost(weight: -5)" defined in tests. +directive @approx(tolerance: Int = 1) on FIELD_DEFINITION + type Query { hero: Character @deprecated - droid(id: ID!): Droid - search(name: String!): SearchResult + droid(id: ID!): Droid @approx(tolerance: null) + search(name: String!): SearchResult @approx searchResults: [SearchResult] } @@ -52,7 +56,7 @@ type Human implements Character { } type Droid implements Character { - name: String! + name: String! @approx primaryFunction: String! friends: [Character] }