graphql 2.5.11 → 2.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/detailed_trace_generator.rb +77 -0
  3. data/lib/generators/graphql/field_extractor.rb +17 -1
  4. data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
  5. data/lib/generators/graphql/templates/schema.erb +2 -1
  6. data/lib/graphql/analysis/query_complexity.rb +31 -15
  7. data/lib/graphql/analysis.rb +20 -13
  8. data/lib/graphql/backtrace/table.rb +10 -1
  9. data/lib/graphql/current.rb +7 -1
  10. data/lib/graphql/dashboard/application_controller.rb +45 -0
  11. data/lib/graphql/dashboard/landings_controller.rb +9 -0
  12. data/lib/graphql/dashboard/statics_controller.rb +31 -0
  13. data/lib/graphql/dashboard/subscriptions.rb +2 -1
  14. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
  15. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
  16. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
  17. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
  18. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
  19. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
  20. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
  21. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
  22. data/lib/graphql/dashboard.rb +14 -73
  23. data/lib/graphql/dataloader/async_dataloader.rb +336 -60
  24. data/lib/graphql/dataloader/null_dataloader.rb +48 -10
  25. data/lib/graphql/dataloader/source.rb +32 -19
  26. data/lib/graphql/dataloader.rb +107 -39
  27. data/lib/graphql/date_encoding_error.rb +1 -1
  28. data/lib/graphql/execution/directive_checks.rb +2 -0
  29. data/lib/graphql/execution/field_resolve_step.rb +804 -0
  30. data/lib/graphql/execution/finalize.rb +229 -0
  31. data/lib/graphql/execution/input_values.rb +333 -0
  32. data/lib/graphql/execution/interpreter/arguments_cache.rb +3 -0
  33. data/lib/graphql/execution/interpreter/handles_raw_value.rb +6 -0
  34. data/lib/graphql/execution/interpreter/resolve.rb +7 -13
  35. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +13 -0
  36. data/lib/graphql/execution/interpreter/runtime.rb +60 -40
  37. data/lib/graphql/execution/interpreter.rb +17 -23
  38. data/lib/graphql/execution/lazy.rb +1 -1
  39. data/lib/graphql/execution/load_argument_step.rb +102 -0
  40. data/lib/graphql/execution/multiplex.rb +1 -1
  41. data/lib/graphql/execution/next.rb +98 -0
  42. data/lib/graphql/execution/prepare_object_step.rb +151 -0
  43. data/lib/graphql/execution/resolve_type_step.rb +27 -0
  44. data/lib/graphql/execution/runner.rb +457 -0
  45. data/lib/graphql/execution/selections_step.rb +95 -0
  46. data/lib/graphql/execution.rb +15 -4
  47. data/lib/graphql/execution_error.rb +17 -12
  48. data/lib/graphql/float_decoding_error.rb +13 -0
  49. data/lib/graphql/float_encoding_error.rb +28 -0
  50. data/lib/graphql/introspection/directive_type.rb +7 -3
  51. data/lib/graphql/introspection/dynamic_fields.rb +5 -1
  52. data/lib/graphql/introspection/entry_points.rb +11 -3
  53. data/lib/graphql/introspection/enum_value_type.rb +5 -5
  54. data/lib/graphql/introspection/field_type.rb +13 -5
  55. data/lib/graphql/introspection/input_value_type.rb +21 -13
  56. data/lib/graphql/introspection/schema_type.rb +6 -2
  57. data/lib/graphql/introspection/type_type.rb +64 -28
  58. data/lib/graphql/invalid_null_error.rb +11 -5
  59. data/lib/graphql/language/block_string.rb +6 -11
  60. data/lib/graphql/language/cache.rb +84 -14
  61. data/lib/graphql/language/document_from_schema_definition.rb +2 -1
  62. data/lib/graphql/language/lexer.rb +27 -17
  63. data/lib/graphql/language/nodes.rb +2 -1
  64. data/lib/graphql/language/parser.rb +1 -1
  65. data/lib/graphql/language.rb +32 -17
  66. data/lib/graphql/pagination/connection.rb +2 -0
  67. data/lib/graphql/pagination/connections.rb +30 -0
  68. data/lib/graphql/pagination/relation_connection.rb +10 -1
  69. data/lib/graphql/query/context.rb +10 -3
  70. data/lib/graphql/query/null_context.rb +9 -3
  71. data/lib/graphql/query/partial.rb +18 -3
  72. data/lib/graphql/query/variable_validation_error.rb +16 -1
  73. data/lib/graphql/query/variables.rb +1 -1
  74. data/lib/graphql/query.rb +12 -3
  75. data/lib/graphql/railtie.rb +2 -1
  76. data/lib/graphql/runtime_error.rb +6 -0
  77. data/lib/graphql/schema/argument.rb +13 -1
  78. data/lib/graphql/schema/build_from_definition.rb +28 -1
  79. data/lib/graphql/schema/directive/feature.rb +4 -0
  80. data/lib/graphql/schema/directive/transform.rb +20 -0
  81. data/lib/graphql/schema/directive.rb +48 -13
  82. data/lib/graphql/schema/field/connection_extension.rb +4 -37
  83. data/lib/graphql/schema/field/scope_extension.rb +18 -13
  84. data/lib/graphql/schema/field.rb +79 -48
  85. data/lib/graphql/schema/field_extension.rb +11 -8
  86. data/lib/graphql/schema/has_single_input_argument.rb +24 -13
  87. data/lib/graphql/schema/input_object.rb +6 -2
  88. data/lib/graphql/schema/interface.rb +26 -0
  89. data/lib/graphql/schema/introspection_system.rb +6 -21
  90. data/lib/graphql/schema/list.rb +5 -1
  91. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -11
  92. data/lib/graphql/schema/member/has_arguments.rb +43 -14
  93. data/lib/graphql/schema/member/has_authorization.rb +35 -0
  94. data/lib/graphql/schema/member/has_dataloader.rb +37 -0
  95. data/lib/graphql/schema/member/has_fields.rb +86 -5
  96. data/lib/graphql/schema/member.rb +5 -0
  97. data/lib/graphql/schema/non_null.rb +1 -1
  98. data/lib/graphql/schema/object.rb +1 -0
  99. data/lib/graphql/schema/printer.rb +1 -1
  100. data/lib/graphql/schema/ractor_shareable.rb +1 -0
  101. data/lib/graphql/schema/relay_classic_mutation.rb +16 -2
  102. data/lib/graphql/schema/resolver.rb +76 -1
  103. data/lib/graphql/schema/subscription.rb +53 -10
  104. data/lib/graphql/schema/timeout.rb +2 -2
  105. data/lib/graphql/schema/validator/allow_blank_validator.rb +3 -3
  106. data/lib/graphql/schema/validator/allow_null_validator.rb +3 -3
  107. data/lib/graphql/schema/validator/exclusion_validator.rb +2 -2
  108. data/lib/graphql/schema/validator/format_validator.rb +3 -3
  109. data/lib/graphql/schema/validator/inclusion_validator.rb +2 -2
  110. data/lib/graphql/schema/validator/length_validator.rb +6 -6
  111. data/lib/graphql/schema/validator/numericality_validator.rb +19 -19
  112. data/lib/graphql/schema/validator/required_validator.rb +38 -5
  113. data/lib/graphql/schema/validator.rb +9 -0
  114. data/lib/graphql/schema/visibility/profile.rb +74 -53
  115. data/lib/graphql/schema/visibility/visit.rb +1 -1
  116. data/lib/graphql/schema/visibility.rb +32 -24
  117. data/lib/graphql/schema/wrapper.rb +11 -1
  118. data/lib/graphql/schema.rb +93 -27
  119. data/lib/graphql/static_validation/base_visitor.rb +90 -66
  120. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  121. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
  122. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
  123. data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
  124. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
  125. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +14 -4
  126. data/lib/graphql/static_validation/rules/fields_will_merge.rb +351 -248
  127. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
  128. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  129. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
  130. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
  131. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
  132. data/lib/graphql/static_validation/validation_context.rb +1 -1
  133. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
  134. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +40 -10
  135. data/lib/graphql/subscriptions/serialize.rb +10 -4
  136. data/lib/graphql/subscriptions.rb +36 -1
  137. data/lib/graphql/testing/helpers.rb +12 -9
  138. data/lib/graphql/testing/mock_action_cable.rb +111 -0
  139. data/lib/graphql/testing.rb +1 -0
  140. data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
  141. data/lib/graphql/tracing/detailed_trace.rb +70 -7
  142. data/lib/graphql/tracing/perfetto_trace.rb +213 -81
  143. data/lib/graphql/tracing/sentry_trace.rb +3 -1
  144. data/lib/graphql/tracing/trace.rb +6 -0
  145. data/lib/graphql/types/float.rb +18 -4
  146. data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
  147. data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
  148. data/lib/graphql/types/relay/has_node_field.rb +13 -8
  149. data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
  150. data/lib/graphql/types/relay/node_behaviors.rb +13 -2
  151. data/lib/graphql/unauthorized_error.rb +9 -1
  152. data/lib/graphql/version.rb +1 -1
  153. data/lib/graphql.rb +8 -2
  154. metadata +23 -59
@@ -8,31 +8,112 @@ module GraphQL
8
8
  # fragments) either correspond to distinct response names or can be merged
9
9
  # without ambiguity.
10
10
  #
11
- # Original Algorithm: https://github.com/graphql/graphql-js/blob/master/src/validation/rules/OverlappingFieldsCanBeMerged.js
11
+ # Optimized algorithm based on:
12
+ # https://tech.new-work.se/graphql-overlapping-fields-can-be-merged-fast-ea6e92e0a01
13
+ #
14
+ # Instead of comparing fields, fields-vs-fragments, and fragments-vs-fragments
15
+ # separately (which leads to exponential recursion through nested fragments),
16
+ # we flatten all fragment spreads into a single field map and compare within it.
12
17
  NO_ARGS = GraphQL::EmptyObjects::EMPTY_HASH
18
+ EXCLUSIVE_COMPARISON = 1
19
+ NONEXCLUSIVE_COMPARISON = 2
20
+
21
+ class Field
22
+ attr_reader :node, :definition, :owner_type, :parents
23
+
24
+ def initialize(node, definition, owner_type, parents)
25
+ @node = node
26
+ @definition = definition
27
+ @owner_type = owner_type
28
+ @parents = parents
29
+ end
13
30
 
14
- Field = Struct.new(:node, :definition, :owner_type, :parents)
15
- FragmentSpread = Struct.new(:name, :parents)
31
+ def return_type
32
+ @return_type ||= @definition&.type
33
+ end
34
+
35
+ def unwrapped_return_type
36
+ @unwrapped_return_type ||= return_type&.unwrap
37
+ end
38
+
39
+ def comparison_key
40
+ @comparison_key ||= [@node, @parents]
41
+ end
42
+ end
16
43
 
17
44
  def initialize(*)
18
45
  super
19
- @visited_fragments = {}
20
- @compared_fragments = {}
21
46
  @conflict_count = 0
47
+ @max_errors = context.max_errors
48
+ @fragments = context.fragments
49
+ # Track which sub-selection node pairs have been compared to prevent
50
+ # infinite recursion with cyclic fragments
51
+ @compared_sub_selections = {}.compare_by_identity
52
+ @compared_field_groups = {}
53
+ @field_group_signatures = {}.compare_by_identity
54
+ @field_selection_signatures = {}.compare_by_identity
55
+ # Cache mutually_exclusive? results for type pairs
56
+ @mutually_exclusive_cache = {}.compare_by_identity
57
+ # Cache collect_fields results for sub-selection comparison
58
+ @sub_fields_cache = {}.compare_by_identity
22
59
  end
23
60
 
24
61
  def on_operation_definition(node, _parent)
25
- setting_errors { conflicts_within_selection_set(node, type_definition) }
62
+ @conflicts = nil
63
+ conflicts_within_selection_set(node, type_definition)
64
+ @conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } }
26
65
  super
27
66
  end
28
67
 
29
68
  def on_field(node, _parent)
30
- setting_errors { conflicts_within_selection_set(node, type_definition) }
69
+ if !node.selections.empty? && selections_may_conflict?(node.selections)
70
+ @conflicts = nil
71
+ conflicts_within_selection_set(node, type_definition)
72
+ @conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } }
73
+ end
31
74
  super
32
75
  end
33
76
 
34
77
  private
35
78
 
79
+ # Quick check: can the direct children of this selection set possibly conflict?
80
+ # If all direct selections are Fields with unique names and no aliases,
81
+ # and there are no fragments, then no response key can have >1 field,
82
+ # so there are no merge conflicts to check at this level.
83
+ def selections_may_conflict?(selections)
84
+ i = 0
85
+ len = selections.size
86
+ while i < len
87
+ sel = selections[i]
88
+ # Fragment spread or inline fragment — needs full check
89
+ return true unless sel.is_a?(GraphQL::Language::Nodes::Field)
90
+
91
+ # Aliased field — could create duplicate response key
92
+ return true if sel.alias
93
+
94
+ i += 1
95
+ end
96
+
97
+ # All are unaliased fields — check for duplicate names
98
+ # For small sets, O(n²) is cheaper than hash allocation
99
+ if len <= 8
100
+ i = 0
101
+ while i < len
102
+ j = i + 1
103
+ name_i = selections[i].name
104
+ while j < len
105
+ return true if selections[j].name == name_i
106
+ j += 1
107
+ end
108
+ i += 1
109
+ end
110
+
111
+ false
112
+ else
113
+ true # Assume potential conflicts for larger sets
114
+ end
115
+ end
116
+
36
117
  def conflicts
37
118
  @conflicts ||= Hash.new do |h, error_type|
38
119
  h[error_type] = Hash.new do |h2, field_name|
@@ -41,177 +122,198 @@ module GraphQL
41
122
  end
42
123
  end
43
124
 
44
- def setting_errors
45
- @conflicts = nil
46
- yield
47
- # don't initialize these if they weren't initialized in the block:
48
- @conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } }
49
- end
50
-
125
+ # Core algorithm: collect ALL fields (expanding fragments inline) into a flat
126
+ # map keyed by response key, then compare within each group.
51
127
  def conflicts_within_selection_set(node, parent_type)
52
128
  return if parent_type.nil?
129
+ return if node.selections.empty?
53
130
 
54
- fields, fragment_spreads = fields_and_fragments_from_selection(node, owner_type: parent_type, parents: nil)
55
-
56
- # (A) Find find all conflicts "within" the fields of this selection set.
57
- find_conflicts_within(fields)
58
-
59
- fragment_spreads.each_with_index do |fragment_spread, i|
60
- are_mutually_exclusive = mutually_exclusive?(
61
- fragment_spread.parents,
62
- [parent_type]
63
- )
64
-
65
- # (B) Then find conflicts between these fields and those represented by
66
- # each spread fragment name found.
67
- find_conflicts_between_fields_and_fragment(
68
- fragment_spread,
69
- fields,
70
- mutually_exclusive: are_mutually_exclusive,
71
- )
72
-
73
- # (C) Then compare this fragment with all other fragments found in this
74
- # selection set to collect conflicts between fragments spread together.
75
- # This compares each item in the list of fragment names to every other
76
- # item in that same list (except for itself).
77
- fragment_spreads[i + 1..-1].each do |fragment_spread2|
78
- are_mutually_exclusive = mutually_exclusive?(
79
- fragment_spread.parents,
80
- fragment_spread2.parents
81
- )
82
-
83
- find_conflicts_between_fragments(
84
- fragment_spread,
85
- fragment_spread2,
86
- mutually_exclusive: are_mutually_exclusive,
87
- )
88
- end
89
- end
131
+ # Collect all fields from this selection set, expanding fragments transitively
132
+ response_keys = collect_fields(node.selections, owner_type: parent_type, parents: [])
133
+
134
+ # Find conflicts within each response key group
135
+ find_conflicts_within(response_keys)
90
136
  end
91
137
 
92
- def find_conflicts_between_fragments(fragment_spread1, fragment_spread2, mutually_exclusive:)
93
- fragment_name1 = fragment_spread1.name
94
- fragment_name2 = fragment_spread2.name
95
- return if fragment_name1 == fragment_name2
138
+ # Collect all fields from selections, expanding fragment spreads inline.
139
+ # Returns a Hash of { response_key => Field | [Field, ...] }
140
+ def collect_fields(selections, owner_type:, parents:)
141
+ response_keys = {}
142
+ collect_fields_inner(selections, owner_type: owner_type, parents: parents, response_keys: response_keys, visited_fragments: nil)
143
+ response_keys
144
+ end
96
145
 
97
- cache_key = compared_fragments_key(
98
- fragment_name1,
99
- fragment_name2,
100
- mutually_exclusive,
101
- )
102
- if @compared_fragments.key?(cache_key)
103
- return
104
- else
105
- @compared_fragments[cache_key] = true
106
- end
146
+ def collect_fields_inner(selections, owner_type:, parents:, response_keys:, visited_fragments:)
147
+ deferred_spreads = nil
148
+ sel_idx = 0
149
+ sel_len = selections.size
107
150
 
108
- fragment1 = context.fragments[fragment_name1]
109
- fragment2 = context.fragments[fragment_name2]
151
+ while sel_idx < sel_len
152
+ sel = selections[sel_idx]
110
153
 
111
- return if fragment1.nil? || fragment2.nil?
154
+ case sel
155
+ when GraphQL::Language::Nodes::Field
156
+ definition = @types.field(owner_type, sel.name)
157
+ key = sel.alias || sel.name
158
+ field = Field.new(sel, definition, owner_type, parents)
159
+ existing = response_keys[key]
160
+
161
+ if existing.nil?
162
+ response_keys[key] = field
163
+ elsif existing.is_a?(Field)
164
+ response_keys[key] = [existing, field]
165
+ else
166
+ existing << field
167
+ end
168
+ when GraphQL::Language::Nodes::InlineFragment
169
+ frag_type = sel.type ? @types.type(sel.type.name) : owner_type
170
+
171
+ if frag_type
172
+ new_parents = parents.dup
173
+ new_parents << frag_type
174
+ collect_fields_inner(sel.selections, owner_type: frag_type, parents: new_parents, response_keys: response_keys, visited_fragments: visited_fragments)
175
+ end
176
+ when GraphQL::Language::Nodes::FragmentSpread
177
+ (deferred_spreads ||= []) << sel
178
+ end
112
179
 
113
- fragment_type1 = context.query.types.type(fragment1.type.name)
114
- fragment_type2 = context.query.types.type(fragment2.type.name)
180
+ sel_idx += 1
181
+ end
115
182
 
116
- return if fragment_type1.nil? || fragment_type2.nil?
183
+ if deferred_spreads
184
+ visited_fragments ||= {}
185
+ sel_idx = 0
186
+ sel_len = deferred_spreads.size
117
187
 
118
- fragment_fields1, fragment_spreads1 = fields_and_fragments_from_selection(
119
- fragment1,
120
- owner_type: fragment_type1,
121
- parents: [*fragment_spread1.parents, fragment_type1]
122
- )
123
- fragment_fields2, fragment_spreads2 = fields_and_fragments_from_selection(
124
- fragment2,
125
- owner_type: fragment_type1,
126
- parents: [*fragment_spread2.parents, fragment_type2]
127
- )
188
+ while sel_idx < sel_len
189
+ sel = deferred_spreads[sel_idx]
190
+ sel_idx += 1
191
+ next if visited_fragments.key?(sel.name)
128
192
 
129
- # (F) First, find all conflicts between these two collections of fields
130
- # (not including any nested fragments).
131
- find_conflicts_between(
132
- fragment_fields1,
133
- fragment_fields2,
134
- mutually_exclusive: mutually_exclusive,
135
- )
193
+ visited_fragments[sel.name] = true
194
+ frag = @fragments[sel.name]
195
+ next unless frag
136
196
 
137
- # (G) Then collect conflicts between the first fragment and any nested
138
- # fragments spread in the second fragment.
139
- fragment_spreads2.each do |fragment_spread|
140
- find_conflicts_between_fragments(
141
- fragment_spread1,
142
- fragment_spread,
143
- mutually_exclusive: mutually_exclusive,
144
- )
145
- end
197
+ frag_type = @types.type(frag.type.name)
198
+ next unless frag_type
146
199
 
147
- # (G) Then collect conflicts between the first fragment and any nested
148
- # fragments spread in the second fragment.
149
- fragment_spreads1.each do |fragment_spread|
150
- find_conflicts_between_fragments(
151
- fragment_spread2,
152
- fragment_spread,
153
- mutually_exclusive: mutually_exclusive,
154
- )
200
+ new_parents = parents.dup
201
+ new_parents << frag_type
202
+ collect_fields_inner(frag.selections, owner_type: frag_type, parents: new_parents, response_keys: response_keys, visited_fragments: visited_fragments)
203
+ end
155
204
  end
156
205
  end
157
206
 
158
- def find_conflicts_between_fields_and_fragment(fragment_spread, fields, mutually_exclusive:)
159
- fragment_name = fragment_spread.name
160
- return if @visited_fragments.key?(fragment_name)
161
- @visited_fragments[fragment_name] = true
207
+ def find_conflicts_within(response_keys)
208
+ response_keys.each do |key, fields|
209
+ next unless fields.is_a?(Array)
210
+
211
+ # Optimization: group fields by signature (name + definition + arguments).
212
+ # Fields with the same signature can only conflict on sub-selections,
213
+ # so we only need to compare one pair within each group.
214
+ if fields.size > 4
215
+ f0 = fields[0]
216
+ all_same = true
217
+ i = 1
218
+ while i < fields.size
219
+ unless fields_same_signature?(f0, fields[i])
220
+ all_same = false
221
+ break
222
+ end
223
+ i += 1
224
+ end
225
+
226
+ if all_same
227
+ find_conflicts_between_selection_groups(key, fields)
228
+ else
229
+ groups = fields.group_by { |f| field_signature(f) }
230
+ unique_groups = groups.values
231
+
232
+ # Compare representatives across different groups
233
+ gi = 0
234
+ while gi < unique_groups.size
235
+ gj = gi + 1
236
+ while gj < unique_groups.size
237
+ find_conflict(key, unique_groups[gi][0], unique_groups[gj][0])
238
+ gj += 1
239
+ end
240
+
241
+ # Within the same group, fields can only conflict on sub-selections.
242
+ group = unique_groups[gi]
243
+ if group.size >= 2
244
+ find_conflicts_between_selection_groups(key, group)
245
+ end
246
+
247
+ gi += 1
248
+ end
249
+ end
250
+ else
251
+ # Small number of fields — original O(n²) is fine
252
+ i = 0
253
+ while i < fields.size
254
+ j = i + 1
255
+ while j < fields.size
256
+ find_conflict(key, fields[i], fields[j])
257
+ j += 1
258
+ end
259
+ i += 1
260
+ end
261
+ end
262
+ end
263
+ end
162
264
 
163
- fragment = context.fragments[fragment_name]
164
- return if fragment.nil?
265
+ def find_conflicts_between_selection_groups(response_key, fields)
266
+ fields_by_selection = {}
267
+ fields.each do |field|
268
+ fields_by_selection[field_selection_signature(field)] ||= field
269
+ end
165
270
 
166
- fragment_type = @types.type(fragment.type.name)
167
- return if fragment_type.nil?
271
+ representatives = fields_by_selection.values
272
+ i = 0
273
+ while i < representatives.size
274
+ j = i + 1
275
+ while j < representatives.size
276
+ find_conflict(response_key, representatives[i], representatives[j])
277
+ j += 1
278
+ end
279
+ i += 1
280
+ end
281
+ end
168
282
 
169
- fragment_fields, fragment_spreads = fields_and_fragments_from_selection(fragment, owner_type: fragment_type, parents: [*fragment_spread.parents, fragment_type])
283
+ def field_selection_signature(field)
284
+ node = field.node
285
+ @field_selection_signatures[node] ||= node.selections.map(&:to_query_string)
286
+ end
170
287
 
171
- # (D) First find any conflicts between the provided collection of fields
172
- # and the collection of fields represented by the given fragment.
173
- find_conflicts_between(
174
- fields,
175
- fragment_fields,
176
- mutually_exclusive: mutually_exclusive,
177
- )
288
+ def fields_same_signature?(f1, f2)
289
+ n1 = f1.node
290
+ n2 = f2.node
178
291
 
179
- # (E) Then collect any conflicts between the provided collection of fields
180
- # and any fragment names found in the given fragment.
181
- fragment_spreads.each do |fragment_spread|
182
- find_conflicts_between_fields_and_fragment(
183
- fragment_spread,
184
- fields,
185
- mutually_exclusive: mutually_exclusive,
186
- )
187
- end
292
+ f1.definition.equal?(f2.definition) &&
293
+ n1.name == n2.name &&
294
+ same_arguments?(n1, n2)
188
295
  end
189
296
 
190
- def find_conflicts_within(response_keys)
191
- response_keys.each do |key, fields|
192
- next if fields.size < 2
193
- # find conflicts within nodes
194
- i = 0
195
- while i < fields.size
196
- j = i + 1
197
- while j < fields.size
198
- find_conflict(key, fields[i], fields[j])
199
- j += 1
200
- end
201
- i += 1
202
- end
297
+ def field_signature(field)
298
+ node = field.node
299
+ defn = field.definition
300
+ args = node.arguments
301
+
302
+ if args.empty?
303
+ [node.name, defn.object_id]
304
+ else
305
+ [node.name, defn.object_id, args.map { |a| [a.name, serialize_arg(a.value)] }]
203
306
  end
204
307
  end
205
308
 
206
309
  def find_conflict(response_key, field1, field2, mutually_exclusive: false)
207
- return if @conflict_count >= context.max_errors
310
+ return if @conflict_count >= @max_errors
208
311
  return if field1.definition.nil? || field2.definition.nil?
209
312
 
210
313
  node1 = field1.node
211
314
  node2 = field2.node
212
315
 
213
- are_mutually_exclusive = mutually_exclusive ||
214
- mutually_exclusive?(field1.parents, field2.parents)
316
+ are_mutually_exclusive = mutually_exclusive || mutually_exclusive?(field1.parents, field2.parents)
215
317
 
216
318
  if !are_mutually_exclusive
217
319
  if node1.name != node2.name
@@ -234,17 +336,20 @@ module GraphQL
234
336
  end
235
337
 
236
338
  if !conflicts[:field].key?(response_key) &&
237
- (t1 = field1.definition&.type) &&
238
- (t2 = field2.definition&.type) &&
339
+ !field1.definition.equal?(field2.definition) &&
340
+ (t1 = field1.return_type) &&
341
+ (t2 = field2.return_type) &&
239
342
  return_types_conflict?(t1, t2)
240
343
 
241
344
  return_error = nil
242
345
  message_override = nil
346
+
243
347
  case @schema.allow_legacy_invalid_return_type_conflicts
244
348
  when false
245
349
  return_error = true
246
350
  when true
247
351
  legacy_handling = @schema.legacy_invalid_return_type_conflicts(@context.query, t1, t2, node1, node2)
352
+
248
353
  case legacy_handling
249
354
  when nil
250
355
  return_error = false
@@ -267,9 +372,11 @@ module GraphQL
267
372
 
268
373
  if return_error
269
374
  conflict = conflicts[:return_type][response_key]
375
+
270
376
  if message_override
271
377
  conflict.message = message_override
272
378
  end
379
+
273
380
  conflict.add_conflict(node1, "`#{t1.to_type_signature}`")
274
381
  conflict.add_conflict(node2, "`#{t2.to_type_signature}`")
275
382
  @conflict_count += 1
@@ -303,121 +410,109 @@ module GraphQL
303
410
  elsif type1.kind.leaf? && type2.kind.leaf?
304
411
  type1 != type2
305
412
  else
306
- # One or more of these are composite types,
307
- # their selections will be validated later on.
308
413
  false
309
414
  end
310
415
  end
311
416
 
417
+ # When two fields with the same response key both have sub-selections,
418
+ # we need to check those sub-selections against each other.
312
419
  def find_conflicts_between_sub_selection_sets(field1, field2, mutually_exclusive:)
313
420
  return if field1.definition.nil? ||
314
421
  field2.definition.nil? ||
315
422
  (field1.node.selections.empty? && field2.node.selections.empty?)
316
423
 
317
- return_type1 = field1.definition.type.unwrap
318
- return_type2 = field2.definition.type.unwrap
319
- parents1 = [return_type1]
320
- parents2 = [return_type2]
321
-
322
- fields, fragment_spreads = fields_and_fragments_from_selection(
323
- field1.node,
324
- owner_type: return_type1,
325
- parents: parents1
326
- )
424
+ node1 = field1.node
425
+ node2 = field2.node
327
426
 
328
- fields2, fragment_spreads2 = fields_and_fragments_from_selection(
329
- field2.node,
330
- owner_type: return_type2,
331
- parents: parents2
332
- )
427
+ # Prevent infinite recursion from cyclic fragments
428
+ return if node1.equal?(node2)
333
429
 
334
- # (H) First, collect all conflicts between these two collections of field.
335
- find_conflicts_between(fields, fields2, mutually_exclusive: mutually_exclusive)
336
-
337
- # (I) Then collect conflicts between the first collection of fields and
338
- # those referenced by each fragment name associated with the second.
339
- fragment_spreads2.each do |fragment_spread|
340
- find_conflicts_between_fields_and_fragment(
341
- fragment_spread,
342
- fields,
343
- mutually_exclusive: mutually_exclusive,
344
- )
430
+ inner = @compared_sub_selections[node1]
431
+ if inner
432
+ return if inner.key?(node2)
433
+ inner[node2] = true
434
+ else
435
+ inner = {}.compare_by_identity
436
+ inner[node2] = true
437
+ @compared_sub_selections[node1] = inner
345
438
  end
346
439
 
347
- # (I) Then collect conflicts between the second collection of fields and
348
- # those referenced by each fragment name associated with the first.
349
- fragment_spreads.each do |fragment_spread|
350
- find_conflicts_between_fields_and_fragment(
351
- fragment_spread,
352
- fields2,
353
- mutually_exclusive: mutually_exclusive,
354
- )
355
- end
440
+ return_type1 = field1.unwrapped_return_type
441
+ return_type2 = field2.unwrapped_return_type
356
442
 
357
- # (J) Also collect conflicts between any fragment names by the first and
358
- # fragment names by the second. This compares each item in the first set of
359
- # names to each item in the second set of names.
360
- fragment_spreads.each do |frag1|
361
- fragment_spreads2.each do |frag2|
362
- find_conflicts_between_fragments(
363
- frag1,
364
- frag2,
365
- mutually_exclusive: mutually_exclusive
366
- )
367
- end
443
+ response_keys1 = cached_sub_fields(node1, return_type1)
444
+ response_keys2 = cached_sub_fields(node2, return_type2)
445
+
446
+ find_conflicts_between(response_keys1, response_keys2, mutually_exclusive: mutually_exclusive)
447
+ end
448
+
449
+ def cached_sub_fields(node, return_type)
450
+ inner = @sub_fields_cache[node]
451
+
452
+ if inner && inner.key?(return_type)
453
+ inner[return_type]
454
+ else
455
+ result = collect_fields(node.selections, owner_type: return_type, parents: [return_type])
456
+ inner ||= {}.compare_by_identity
457
+ inner[return_type] = result
458
+ @sub_fields_cache[node] = inner
459
+ result
368
460
  end
369
461
  end
370
462
 
371
463
  def find_conflicts_between(response_keys, response_keys2, mutually_exclusive:)
372
464
  response_keys.each do |key, fields|
373
465
  fields2 = response_keys2[key]
374
- if fields2
375
- fields.each do |field|
376
- fields2.each do |field2|
377
- find_conflict(
378
- key,
379
- field,
380
- field2,
381
- mutually_exclusive: mutually_exclusive,
382
- )
383
- end
466
+ next unless fields2
467
+ next if field_groups_already_compared?(fields, fields2, mutually_exclusive)
468
+
469
+ fields_arr = fields.is_a?(Field) ? [fields] : fields
470
+ fields2_arr = fields2.is_a?(Field) ? [fields2] : fields2
471
+
472
+ fields_arr.each do |field|
473
+ fields2_arr.each do |field2|
474
+ find_conflict(
475
+ key,
476
+ field,
477
+ field2,
478
+ mutually_exclusive: mutually_exclusive,
479
+ )
384
480
  end
385
481
  end
386
482
  end
387
483
  end
388
484
 
389
- NO_SELECTIONS = [GraphQL::EmptyObjects::EMPTY_HASH, GraphQL::EmptyObjects::EMPTY_ARRAY].freeze
485
+ def field_groups_already_compared?(fields, fields2, mutually_exclusive)
486
+ signature1 = field_group_signature(fields)
487
+ signature2 = field_group_signature(fields2)
488
+ previous_comparisons = @compared_field_groups[signature1]
489
+ comparison_state = previous_comparisons && previous_comparisons[signature2]
390
490
 
391
- def fields_and_fragments_from_selection(node, owner_type:, parents:)
392
- if node.selections.empty?
393
- NO_SELECTIONS
491
+ if mutually_exclusive
492
+ return true if comparison_state
493
+ new_state = EXCLUSIVE_COMPARISON
394
494
  else
395
- parents ||= []
396
- fields, fragment_spreads = find_fields_and_fragments(node.selections, owner_type: owner_type, parents: parents, fields: [], fragment_spreads: [])
397
- response_keys = fields.group_by { |f| f.node.alias || f.node.name }
398
- [response_keys, fragment_spreads]
495
+ return true if comparison_state == NONEXCLUSIVE_COMPARISON
496
+ new_state = NONEXCLUSIVE_COMPARISON
399
497
  end
498
+
499
+ previous_comparisons ||= (@compared_field_groups[signature1] = {})
500
+ previous_comparisons[signature2] = new_state
501
+
502
+ reverse_comparisons = @compared_field_groups[signature2] ||= {}
503
+ reverse_comparisons[signature1] = new_state
504
+ false
400
505
  end
401
506
 
402
- def find_fields_and_fragments(selections, owner_type:, parents:, fields:, fragment_spreads:)
403
- selections.each do |node|
404
- case node
405
- when GraphQL::Language::Nodes::Field
406
- definition = @types.field(owner_type, node.name)
407
- fields << Field.new(node, definition, owner_type, parents)
408
- when GraphQL::Language::Nodes::InlineFragment
409
- fragment_type = node.type ? @types.type(node.type.name) : owner_type
410
- find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: fragment_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type
411
- when GraphQL::Language::Nodes::FragmentSpread
412
- fragment_spreads << FragmentSpread.new(node.name, parents)
413
- end
507
+ def field_group_signature(fields)
508
+ if fields.is_a?(Field)
509
+ fields.comparison_key
510
+ else
511
+ @field_group_signatures[fields] ||= fields.map(&:comparison_key)
414
512
  end
415
-
416
- [fields, fragment_spreads]
417
513
  end
418
514
 
419
515
  def same_arguments?(field1, field2)
420
- # Check for incompatible / non-identical arguments on this node:
421
516
  arguments1 = field1.arguments
422
517
  arguments2 = field2.arguments
423
518
 
@@ -450,39 +545,47 @@ module GraphQL
450
545
  serialized_args
451
546
  end
452
547
 
453
- def compared_fragments_key(frag1, frag2, exclusive)
454
- # Cache key to not compare two fragments more than once.
455
- # The key includes both fragment names sorted (this way we
456
- # avoid computing "A vs B" and "B vs A"). It also includes
457
- # "exclusive" since the result may change depending on the parent_type
458
- "#{[frag1, frag2].sort.join('-')}-#{exclusive}"
459
- end
460
-
461
548
  # Given two list of parents, find out if they are mutually exclusive
462
- # In this context, `parents` represents the "self scope" of the field,
463
- # what types may be found at this point in the query.
464
549
  def mutually_exclusive?(parents1, parents2)
465
550
  if parents1.empty? || parents2.empty?
466
551
  false
467
552
  elsif parents1.length == parents2.length
468
- parents1.length.times.any? do |i|
553
+ i = 0
554
+ len = parents1.length
555
+
556
+ while i < len
469
557
  type1 = parents1[i - 1]
470
558
  type2 = parents2[i - 1]
471
- if type1 == type2
472
- # If the types we're comparing are the same type,
473
- # then they aren't mutually exclusive
474
- false
475
- else
476
- # Check if these two scopes have _any_ types in common.
477
- possible_right_types = context.types.possible_types(type1)
478
- possible_left_types = context.types.possible_types(type2)
479
- (possible_right_types & possible_left_types).empty?
559
+ unless type1.equal?(type2)
560
+ inner = @mutually_exclusive_cache[type1]
561
+ if inner
562
+ cached = inner[type2]
563
+ if cached.nil?
564
+ cached = types_mutually_exclusive?(type1, type2)
565
+ inner[type2] = cached
566
+ end
567
+ else
568
+ cached = types_mutually_exclusive?(type1, type2)
569
+ inner = {}.compare_by_identity
570
+ inner[type2] = cached
571
+ @mutually_exclusive_cache[type1] = inner
572
+ end
573
+ return true if cached
480
574
  end
575
+ i += 1
481
576
  end
577
+
578
+ false
482
579
  else
483
580
  true
484
581
  end
485
582
  end
583
+
584
+ def types_mutually_exclusive?(type1, type2)
585
+ possible_right_types = @types.possible_types(type1)
586
+ possible_left_types = @types.possible_types(type2)
587
+ (possible_right_types & possible_left_types).empty?
588
+ end
486
589
  end
487
590
  end
488
591
  end