Skip to content

Commit 92517c5

Browse files
authored
Merge pull request rmosolgo#4449 from rmosolgo/better-is-non-null-at-runtime
Pass is_non_null instead of tracking a list of non-null result names
2 parents 7a734f0 + 4b9b55b commit 92517c5

File tree

2 files changed

+33
-44
lines changed

2 files changed

+33
-44
lines changed

benchmark/run.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,13 @@ def self.profile
132132
def self.profile_large_introspection
133133
schema = SILLY_LARGE_SCHEMA
134134
Benchmark.ips do |x|
135+
x.config(time: 10)
135136
x.report("Run large introspection") {
136137
schema.to_json
137138
}
138139
end
139140

140-
result = StackProf.run(mode: :wall, interval: 10) do
141+
result = StackProf.run(mode: :wall) do
141142
schema.to_json
142143
end
143144
StackProf::Report.new(result).print_text

lib/graphql/execution/interpreter/runtime.rb

Lines changed: 31 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ def initialize
2222
end
2323

2424
module GraphQLResult
25-
def initialize(result_name, parent_result)
25+
def initialize(result_name, parent_result, is_non_null_in_parent)
2626
@graphql_parent = parent_result
2727
if parent_result && parent_result.graphql_dead
2828
@graphql_dead = true
2929
end
3030
@graphql_result_name = result_name
31+
@graphql_is_non_null_in_parent = is_non_null_in_parent
3132
# Jump through some hoops to avoid creating this duplicate storage if at all possible.
3233
@graphql_metadata = nil
3334
end
@@ -42,22 +43,14 @@ def build_path(path_array)
4243
end
4344

4445
attr_accessor :graphql_dead
45-
attr_reader :graphql_parent, :graphql_result_name
46-
47-
# Although these are used by only one of the Result classes,
48-
# it's handy to have the methods implemented on both (even though they just return `nil`)
49-
# because it makes it easy to check if anything is assigned.
50-
# @return [nil, Array<String>]
51-
attr_accessor :graphql_non_null_field_names
52-
# @return [nil, true]
53-
attr_accessor :graphql_non_null_list_items
46+
attr_reader :graphql_parent, :graphql_result_name, :graphql_is_non_null_in_parent
5447

5548
# @return [Hash] Plain-Ruby result data (`@graphql_metadata` contains Result wrapper objects)
5649
attr_accessor :graphql_result_data
5750
end
5851

5952
class GraphQLResultHash
60-
def initialize(_result_name, _parent_result)
53+
def initialize(_result_name, _parent_result, _is_non_null_in_parent)
6154
super
6255
@graphql_result_data = {}
6356
end
@@ -145,7 +138,7 @@ def merge_into(into_result)
145138
class GraphQLResultArray
146139
include GraphQLResult
147140

148-
def initialize(_result_name, _parent_result)
141+
def initialize(_result_name, _parent_result, _is_non_null_in_parent)
149142
super
150143
@graphql_result_data = []
151144
end
@@ -209,7 +202,7 @@ def initialize(query:, lazies_at_depth:)
209202
@schema = query.schema
210203
@context = query.context
211204
@multiplex_context = query.multiplex.context
212-
@response = GraphQLResultHash.new(nil, nil)
205+
@response = GraphQLResultHash.new(nil, nil, false)
213206
# Identify runtime directives by checking which of this schema's directives have overridden `def self.resolve`
214207
@runtime_directive_names = []
215208
noop_resolve_owner = GraphQL::Schema::Directive.singleton_class
@@ -276,7 +269,7 @@ def run_eager
276269
# directly evaluated and the results can be written right into the main response hash.
277270
tap_or_each(gathered_selections) do |selections, is_selection_array|
278271
if is_selection_array
279-
selection_response = GraphQLResultHash.new(nil, nil)
272+
selection_response = GraphQLResultHash.new(nil, nil, false)
280273
final_response = @response
281274
else
282275
selection_response = @response
@@ -442,9 +435,6 @@ def evaluate_selection(result_name, field_ast_nodes_or_ast_node, owner_object, o
442435
# the field's return type at this path in order
443436
# to propagate `null`
444437
return_type_non_null = return_type.non_null?
445-
if return_type_non_null
446-
(selections_result.graphql_non_null_field_names ||= []).push(result_name)
447-
end
448438
# Set this before calling `run_with_directives`, so that the directive can have the latest path
449439
st = get_current_runtime_state
450440
st.current_field = field_defn
@@ -589,13 +579,9 @@ def dead_result?(selection_result)
589579
selection_result.graphql_dead # || ((parent = selection_result.graphql_parent) && parent.graphql_dead)
590580
end
591581

592-
def set_result(selection_result, result_name, value, is_child_result)
582+
def set_result(selection_result, result_name, value, is_child_result, is_non_null)
593583
if !dead_result?(selection_result)
594-
if value.nil? &&
595-
( # there are two conditions under which `nil` is not allowed in the response:
596-
(selection_result.graphql_non_null_list_items) || # this value would be written into a list that doesn't allow nils
597-
((nn = selection_result.graphql_non_null_field_names) && nn.include?(result_name)) # this value would be written into a field that doesn't allow nils
598-
)
584+
if value.nil? && is_non_null
599585
# This is an invalid nil that should be propagated
600586
# One caller of this method passes a block,
601587
# namely when application code returns a `nil` to GraphQL and it doesn't belong there.
@@ -605,11 +591,12 @@ def set_result(selection_result, result_name, value, is_child_result)
605591
# TODO the code is trying to tell me something.
606592
yield if block_given?
607593
parent = selection_result.graphql_parent
608-
name_in_parent = selection_result.graphql_result_name
609594
if parent.nil? # This is a top-level result hash
610595
@response = nil
611596
else
612-
set_result(parent, name_in_parent, nil, false)
597+
name_in_parent = selection_result.graphql_result_name
598+
is_non_null_in_parent = selection_result.graphql_is_non_null_in_parent
599+
set_result(parent, name_in_parent, nil, false, is_non_null_in_parent)
613600
set_graphql_dead(selection_result)
614601
end
615602
elsif is_child_result
@@ -651,13 +638,13 @@ def continue_value(value, parent_type, field, is_non_null, ast_node, result_name
651638
case value
652639
when nil
653640
if is_non_null
654-
set_result(selection_result, result_name, nil, false) do
641+
set_result(selection_result, result_name, nil, false, is_non_null) do
655642
# This block is called if `result_name` is not dead. (Maybe a previous invalid nil caused it be marked dead.)
656643
err = parent_type::InvalidNullError.new(parent_type, field, value)
657644
schema.type_error(err, context)
658645
end
659646
else
660-
set_result(selection_result, result_name, nil, false)
647+
set_result(selection_result, result_name, nil, false, is_non_null)
661648
end
662649
HALT
663650
when GraphQL::Error
@@ -670,7 +657,7 @@ def continue_value(value, parent_type, field, is_non_null, ast_node, result_name
670657
value.ast_node ||= ast_node
671658
context.errors << value
672659
if selection_result
673-
set_result(selection_result, result_name, nil, false)
660+
set_result(selection_result, result_name, nil, false, is_non_null)
674661
end
675662
end
676663
HALT
@@ -724,9 +711,9 @@ def continue_value(value, parent_type, field, is_non_null, ast_node, result_name
724711
if selection_result
725712
if list_type_at_all
726713
result_without_errors = value.map { |v| v.is_a?(GraphQL::ExecutionError) ? nil : v }
727-
set_result(selection_result, result_name, result_without_errors, false)
714+
set_result(selection_result, result_name, result_without_errors, false, is_non_null)
728715
else
729-
set_result(selection_result, result_name, nil, false)
716+
set_result(selection_result, result_name, nil, false, is_non_null)
730717
end
731718
end
732719
end
@@ -736,7 +723,7 @@ def continue_value(value, parent_type, field, is_non_null, ast_node, result_name
736723
end
737724
when GraphQL::Execution::Interpreter::RawValue
738725
# Write raw value directly to the response without resolving nested objects
739-
set_result(selection_result, result_name, value.resolve, false)
726+
set_result(selection_result, result_name, value.resolve, false, is_non_null)
740727
HALT
741728
else
742729
value
@@ -764,7 +751,7 @@ def continue_field(value, owner_type, field, current_type, ast_node, next_select
764751
rescue StandardError => err
765752
schema.handle_or_reraise(context, err)
766753
end
767-
set_result(selection_result, result_name, r, false)
754+
set_result(selection_result, result_name, r, false, is_non_null)
768755
r
769756
when "UNION", "INTERFACE"
770757
resolved_type_or_lazy = resolve_type(current_type, value)
@@ -782,7 +769,7 @@ def continue_field(value, owner_type, field, current_type, ast_node, next_select
782769
err_class = current_type::UnresolvedTypeError
783770
type_error = err_class.new(resolved_value, field, parent_type, resolved_type, possible_types)
784771
schema.type_error(type_error, context)
785-
set_result(selection_result, result_name, nil, false)
772+
set_result(selection_result, result_name, nil, false, is_non_null)
786773
nil
787774
else
788775
continue_field(resolved_value, owner_type, field, resolved_type, ast_node, next_selections, is_non_null, owner_object, arguments, result_name, selection_result)
@@ -797,8 +784,8 @@ def continue_field(value, owner_type, field, current_type, ast_node, next_select
797784
after_lazy(object_proxy, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result) do |inner_object|
798785
continue_value = continue_value(inner_object, owner_type, field, is_non_null, ast_node, result_name, selection_result)
799786
if HALT != continue_value
800-
response_hash = GraphQLResultHash.new(result_name, selection_result)
801-
set_result(selection_result, result_name, response_hash, true)
787+
response_hash = GraphQLResultHash.new(result_name, selection_result, is_non_null)
788+
set_result(selection_result, result_name, response_hash, true, is_non_null)
802789
gathered_selections = gather_selections(continue_value, current_type, next_selections)
803790
# There are two possibilities for `gathered_selections`:
804791
# 1. All selections of this object should be evaluated together (there are no runtime directives modifying execution).
@@ -810,7 +797,7 @@ def continue_field(value, owner_type, field, current_type, ast_node, next_select
810797
# (Technically, it's possible that one of those entries _doesn't_ require isolation.)
811798
tap_or_each(gathered_selections) do |selections, is_selection_array|
812799
if is_selection_array
813-
this_result = GraphQLResultHash.new(result_name, selection_result)
800+
this_result = GraphQLResultHash.new(result_name, selection_result, is_non_null)
814801
final_result = response_hash
815802
else
816803
this_result = response_hash
@@ -843,12 +830,12 @@ def continue_field(value, owner_type, field, current_type, ast_node, next_select
843830
# This is true for objects, unions, and interfaces
844831
use_dataloader_job = !inner_type.unwrap.kind.input?
845832
inner_type_non_null = inner_type.non_null?
846-
response_list = GraphQLResultArray.new(result_name, selection_result)
847-
response_list.graphql_non_null_list_items = inner_type_non_null
848-
set_result(selection_result, result_name, response_list, true)
849-
idx = 0
833+
response_list = GraphQLResultArray.new(result_name, selection_result, is_non_null)
834+
set_result(selection_result, result_name, response_list, true, is_non_null)
835+
idx = nil
850836
list_value = begin
851837
value.each do |inner_value|
838+
idx ||= 0
852839
this_idx = idx
853840
idx += 1
854841
if use_dataloader_job
@@ -879,8 +866,9 @@ def continue_field(value, owner_type, field, current_type, ast_node, next_select
879866
ex_err
880867
end
881868
end
882-
883-
continue_value(list_value, owner_type, field, inner_type.non_null?, ast_node, result_name, selection_result)
869+
# Detect whether this error came while calling `.each` (before `idx` is set) or while running list *items* (after `idx` is set)
870+
error_is_non_null = idx.nil? ? is_non_null : inner_type.non_null?
871+
continue_value(list_value, owner_type, field, error_is_non_null, ast_node, result_name, selection_result)
884872
else
885873
raise "Invariant: Unhandled type kind #{current_type.kind} (#{current_type})"
886874
end
@@ -1007,7 +995,7 @@ def after_lazy(lazy_obj, field:, owner_object:, arguments:, ast_node:, result:,
1007995
if eager
1008996
lazy.value
1009997
else
1010-
set_result(result, result_name, lazy, false)
998+
set_result(result, result_name, lazy, false, false) # is_non_null is irrelevant here
1011999
current_depth = 0
10121000
while result
10131001
current_depth += 1

0 commit comments

Comments
 (0)