Skip to content

Commit bd94574

Browse files
committed
complexity cost for v3.
1 parent 33bc196 commit bd94574

File tree

2 files changed

+132
-26
lines changed

2 files changed

+132
-26
lines changed

lib/graphql/analysis/ast/query_complexity.rb

+31-24
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ def initialize(parent_type, field_definition, query, response_path)
4444
def own_complexity(child_complexity)
4545
@field_definition.calculate_complexity(query: @query, nodes: @nodes, child_complexity: child_complexity)
4646
end
47+
48+
def composite?
49+
!empty?
50+
end
4751
end
4852

4953
def on_enter_field(node, parent, visitor)
@@ -145,35 +149,38 @@ def merged_max_complexity(query, inner_selections)
145149

146150
# Add up the total cost for each unique field name's coalesced selections
147151
unique_field_keys.each_key.reduce(0) do |total, field_key|
148-
composite_scopes = nil
149-
field_cost = 0
150-
151-
# Collect composite selection scopes for further aggregation,
152-
# leaf selections report their costs directly.
153-
inner_selections.each do |inner_selection|
154-
child_scope = inner_selection[field_key]
155-
next unless child_scope
156-
157-
# Empty child scopes are leaf nodes with zero child complexity.
158-
if child_scope.empty?
159-
field_cost = child_scope.own_complexity(0)
160-
field_complexity(child_scope, max_complexity: field_cost, child_complexity: nil)
152+
# Collect all child scopes for this field key;
153+
# all keys come with at least one scope.
154+
child_scopes = inner_selections.filter_map { _1[field_key] }
155+
156+
# Compute maximum possible cost of child selections;
157+
# composites merge their maximums, while leaf scopes are always zero.
158+
# FieldsWillMerge validation assures all scopes are uniformly one or the other.
159+
maximum_children_cost = if child_scopes.any?(&:composite?)
160+
merged_max_complexity_for_scopes(query, child_scopes)
161+
else
162+
0
163+
end
164+
165+
# Identify the maximum cost and scope among possibilities
166+
maximum_cost = 0
167+
maximum_scope = child_scopes.reduce(child_scopes.last) do |max_scope, possible_scope|
168+
scope_cost = possible_scope.own_complexity(maximum_children_cost)
169+
if scope_cost > maximum_cost
170+
maximum_cost = scope_cost
171+
possible_scope
161172
else
162-
composite_scopes ||= []
163-
composite_scopes << child_scope
173+
max_scope
164174
end
165175
end
166176

167-
if composite_scopes
168-
child_complexity = merged_max_complexity_for_scopes(query, composite_scopes)
169-
170-
# This is the last composite scope visited; assume it's representative (for backwards compatibility).
171-
# Note: it would be more correct to score each composite scope and use the maximum possibility.
172-
field_cost = composite_scopes.last.own_complexity(child_complexity)
173-
field_complexity(composite_scopes.last, max_complexity: field_cost, child_complexity: child_complexity)
174-
end
177+
field_complexity(
178+
maximum_scope,
179+
max_complexity: maximum_cost,
180+
child_complexity: maximum_children_cost,
181+
)
175182

176-
total + field_cost
183+
total + maximum_cost
177184
end
178185
end
179186
end

spec/graphql/analysis/ast/query_complexity_spec.rb

+101-2
Original file line numberDiff line numberDiff line change
@@ -642,10 +642,109 @@ def field_complexity(scoped_type_complexity, max_complexity:, child_complexity:)
642642
field_complexities = reduce_result.first
643643

644644
assert_equal({
645-
['cheese', 'id'] => { max_complexity: 1, child_complexity: nil },
646-
['cheese', 'flavor'] => { max_complexity: 1, child_complexity: nil },
645+
['cheese', 'id'] => { max_complexity: 1, child_complexity: 0 },
646+
['cheese', 'flavor'] => { max_complexity: 1, child_complexity: 0 },
647647
['cheese'] => { max_complexity: 3, child_complexity: 2 },
648648
}, field_complexities)
649649
end
650650
end
651+
652+
describe "maximum of possible scopes regardless of selection order" do
653+
class MaxOfPossibleScopes < GraphQL::Schema
654+
class Cheese < GraphQL::Schema::Object
655+
field :kind, String
656+
end
657+
658+
module Producer
659+
include GraphQL::Schema::Interface
660+
field :cheese, Cheese, complexity: 5
661+
field :name, String, complexity: 5
662+
end
663+
664+
class Farm < GraphQL::Schema::Object
665+
implements Producer
666+
field :cheese, Cheese, complexity: 10
667+
field :name, String, complexity: 10
668+
end
669+
670+
class Entity < GraphQL::Schema::Union
671+
possible_types Farm
672+
end
673+
674+
class Query < GraphQL::Schema::Object
675+
field :entity, Entity
676+
end
677+
678+
def self.resolve_type
679+
Farm
680+
end
681+
682+
def self.cost(query_string)
683+
GraphQL::Analysis::AST.analyze_query(
684+
GraphQL::Query.new(self, query_string),
685+
[GraphQL::Analysis::AST::QueryComplexity],
686+
).first
687+
end
688+
689+
query(Query)
690+
orphan_types(Producer)
691+
end
692+
693+
it "uses maximum of composite fields, regardless of selection order" do
694+
a = MaxOfPossibleScopes.cost(%|
695+
{
696+
entity {
697+
...on Producer { cheese { kind } }
698+
...on Farm { cheese { kind } }
699+
}
700+
}
701+
|)
702+
703+
b = MaxOfPossibleScopes.cost(%|
704+
{
705+
entity {
706+
...on Farm { cheese { kind } }
707+
...on Producer { cheese { kind } }
708+
}
709+
}
710+
|)
711+
712+
assert_equal 0, a - b
713+
end
714+
715+
it "uses maximum of leaf fields, regardless of selection order" do
716+
a = MaxOfPossibleScopes.cost(%|
717+
{
718+
entity {
719+
...on Producer { name }
720+
...on Farm { name }
721+
}
722+
}
723+
|)
724+
725+
b = MaxOfPossibleScopes.cost(%|
726+
{
727+
entity {
728+
...on Farm { name }
729+
...on Producer { name }
730+
}
731+
}
732+
|)
733+
734+
assert_equal 0, a - b
735+
end
736+
737+
it "invalid mismatched scope types will still compute" do
738+
cost = MaxOfPossibleScopes.cost(%|
739+
{
740+
entity {
741+
...on Farm { cheese { kind } }
742+
...on Producer { cheese: name }
743+
}
744+
}
745+
|)
746+
747+
assert_equal 12, cost
748+
end
749+
end
651750
end

0 commit comments

Comments
 (0)