Skip to content

Commit a65fe3c

Browse files
authored
Merge pull request #5183 from rmosolgo/partial-execution
Add GraphQL::Query::Partial
2 parents ba7b2c4 + e456268 commit a65fe3c

File tree

9 files changed

+795
-82
lines changed

9 files changed

+795
-82
lines changed

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ if RUBY_VERSION >= "3.0"
1313
gem "evt"
1414
end
1515

16-
if RUBY_VERSION >= "3.1.1"
16+
if RUBY_VERSION >= "3.2.0"
1717
gem "async", "~>2.0"
1818
end
1919

lib/graphql/execution/interpreter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def run_all(schema, query_options, context: {}, max_complexity: schema.max_compl
2626
query = case opts
2727
when Hash
2828
schema.query_class.new(schema, nil, **opts)
29-
when GraphQL::Query
29+
when GraphQL::Query, GraphQL::Query::Partial
3030
opts
3131
else
3232
raise "Expected Hash or GraphQL::Query, not #{opts.class} (#{opts.inspect})"

lib/graphql/execution/interpreter/runtime.rb

Lines changed: 123 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -57,53 +57,142 @@ def initialize(query:, lazies_at_depth:)
5757
end
5858

5959
def final_result
60-
@response && @response.graphql_result_data
60+
@response.respond_to?(:graphql_result_data) ? @response.graphql_result_data : @response
6161
end
6262

6363
def inspect
6464
"#<#{self.class.name} response=#{@response.inspect}>"
6565
end
6666

67-
# This _begins_ the execution. Some deferred work
68-
# might be stored up in lazies.
6967
# @return [void]
7068
def run_eager
71-
root_operation = query.selected_operation
72-
root_op_type = root_operation.operation_type || "query"
73-
root_type = schema.root_type_for_operation(root_op_type)
74-
runtime_object = root_type.wrap(query.root_value, context)
75-
runtime_object = schema.sync_lazy(runtime_object)
76-
is_eager = root_op_type == "mutation"
77-
@response = GraphQLResultHash.new(nil, root_type, runtime_object, nil, false, root_operation.selections, is_eager, root_operation, nil, nil)
78-
st = get_current_runtime_state
79-
st.current_result = @response
80-
81-
if runtime_object.nil?
82-
# Root .authorized? returned false.
83-
@response = nil
69+
root_type = query.root_type
70+
case query
71+
when GraphQL::Query
72+
ast_node = query.selected_operation
73+
selections = ast_node.selections
74+
object = query.root_value
75+
is_eager = ast_node.operation_type == "mutation"
76+
base_path = nil
77+
when GraphQL::Query::Partial
78+
ast_node = query.ast_nodes.first
79+
selections = query.ast_nodes.map(&:selections).inject(&:+)
80+
object = query.object
81+
is_eager = false
82+
base_path = query.path
8483
else
85-
call_method_on_directives(:resolve, runtime_object, root_operation.directives) do # execute query level directives
86-
each_gathered_selections(@response) do |selections, is_selection_array, ordered_result_keys|
87-
@response.ordered_result_keys ||= ordered_result_keys
88-
if is_selection_array
89-
selection_response = GraphQLResultHash.new(nil, root_type, runtime_object, nil, false, selections, is_eager, root_operation, nil, nil)
90-
selection_response.ordered_result_keys = ordered_result_keys
91-
final_response = @response
92-
else
93-
selection_response = @response
94-
final_response = nil
95-
end
84+
raise ArgumentError, "Unexpected Runnable, can't execute: #{query.class} (#{query.inspect})"
85+
end
86+
object = schema.sync_lazy(object) # TODO test query partial with lazy root object
87+
runtime_state = get_current_runtime_state
88+
case root_type.kind.name
89+
when "OBJECT"
90+
object_proxy = root_type.wrap(object, context)
91+
object_proxy = schema.sync_lazy(object_proxy)
92+
if object_proxy.nil?
93+
@response = nil
94+
else
95+
@response = GraphQLResultHash.new(nil, root_type, object_proxy, nil, false, selections, is_eager, ast_node, nil, nil)
96+
@response.base_path = base_path
97+
runtime_state.current_result = @response
98+
call_method_on_directives(:resolve, object, ast_node.directives) do
99+
each_gathered_selections(@response) do |selections, is_selection_array, ordered_result_keys|
100+
@response.ordered_result_keys ||= ordered_result_keys
101+
if is_selection_array
102+
selection_response = GraphQLResultHash.new(nil, root_type, object_proxy, nil, false, selections, is_eager, ast_node, nil, nil)
103+
selection_response.ordered_result_keys = ordered_result_keys
104+
final_response = @response
105+
else
106+
selection_response = @response
107+
final_response = nil
108+
end
96109

97-
@dataloader.append_job {
98-
evaluate_selections(
99-
selections,
100-
selection_response,
101-
final_response,
102-
nil,
110+
@dataloader.append_job {
111+
evaluate_selections(
112+
selections,
113+
selection_response,
114+
final_response,
115+
nil,
116+
)
117+
}
118+
end
119+
end
120+
end
121+
when "LIST"
122+
inner_type = root_type.unwrap
123+
case inner_type.kind.name
124+
when "SCALAR", "ENUM"
125+
result_name = ast_node.alias || ast_node.name
126+
owner_type = query.field_definition.owner
127+
selection_result = GraphQLResultHash.new(nil, owner_type, nil, nil, false, EmptyObjects::EMPTY_ARRAY, false, ast_node, nil, nil)
128+
selection_result.base_path = base_path
129+
selection_result.ordered_result_keys = [result_name]
130+
runtime_state = get_current_runtime_state
131+
runtime_state.current_result = selection_result
132+
runtime_state.current_result_name = result_name
133+
field_defn = query.field_definition
134+
continue_value = continue_value(object, field_defn, false, ast_node, result_name, selection_result)
135+
if HALT != continue_value
136+
continue_field(continue_value, owner_type, field_defn, root_type, ast_node, nil, false, nil, nil, result_name, selection_result, false, runtime_state) # rubocop:disable Metrics/ParameterLists
137+
end
138+
@response = selection_result[result_name]
139+
else
140+
@response = GraphQLResultArray.new(nil, root_type, nil, nil, false, selections, false, ast_node, nil, nil)
141+
@response.base_path = base_path
142+
idx = nil
143+
object.each do |inner_value|
144+
idx ||= 0
145+
this_idx = idx
146+
idx += 1
147+
@dataloader.append_job do
148+
runtime_state.current_result_name = this_idx
149+
runtime_state.current_result = @response
150+
continue_field(
151+
inner_value, root_type, nil, inner_type, nil, @response.graphql_selections, false, object_proxy,
152+
nil, this_idx, @response, false, runtime_state
103153
)
104-
}
154+
end
155+
end
156+
end
157+
when "SCALAR", "ENUM"
158+
result_name = ast_node.alias || ast_node.name
159+
owner_type = query.field_definition.owner
160+
selection_result = GraphQLResultHash.new(nil, query.parent_type, nil, nil, false, EmptyObjects::EMPTY_ARRAY, false, ast_node, nil, nil)
161+
selection_result.ordered_result_keys = [result_name]
162+
selection_result.base_path = base_path
163+
runtime_state = get_current_runtime_state
164+
runtime_state.current_result = selection_result
165+
runtime_state.current_result_name = result_name
166+
field_defn = query.field_definition
167+
continue_value = continue_value(object, field_defn, false, ast_node, result_name, selection_result)
168+
if HALT != continue_value
169+
continue_field(continue_value, owner_type, field_defn, query.root_type, ast_node, nil, false, nil, nil, result_name, selection_result, false, runtime_state) # rubocop:disable Metrics/ParameterLists
170+
end
171+
@response = selection_result[result_name]
172+
when "UNION", "INTERFACE"
173+
resolved_type, _resolved_obj = resolve_type(root_type, object)
174+
resolved_type = schema.sync_lazy(resolved_type)
175+
object_proxy = resolved_type.wrap(object, context)
176+
object_proxy = schema.sync_lazy(object_proxy)
177+
@response = GraphQLResultHash.new(nil, resolved_type, object_proxy, nil, false, selections, false, query.ast_nodes.first, nil, nil)
178+
@response.base_path = base_path
179+
each_gathered_selections(@response) do |selections, is_selection_array, ordered_result_keys|
180+
@response.ordered_result_keys ||= ordered_result_keys
181+
if is_selection_array == true
182+
raise "This isn't supported yet"
105183
end
184+
185+
@dataloader.append_job {
186+
evaluate_selections(
187+
selections,
188+
@response,
189+
nil,
190+
runtime_state,
191+
)
192+
}
106193
end
194+
else
195+
raise "Invariant: unsupported type kind for partial execution: #{root_type.kind.inspect} (#{root_type})"
107196
end
108197
nil
109198
end

lib/graphql/execution/interpreter/runtime/graphql_result.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,25 @@ def initialize(result_name, result_type, application_value, parent_result, is_no
2121
@graphql_metadata = nil
2222
@graphql_selections = selections
2323
@graphql_is_eager = is_eager
24+
@base_path = nil
2425
end
2526

27+
# TODO test full path in Partial
28+
attr_writer :base_path
29+
2630
def path
2731
@path ||= build_path([])
2832
end
2933

3034
def build_path(path_array)
3135
graphql_result_name && path_array.unshift(graphql_result_name)
32-
@graphql_parent ? @graphql_parent.build_path(path_array) : path_array
36+
if @graphql_parent
37+
@graphql_parent.build_path(path_array)
38+
elsif @base_path
39+
@base_path + path_array
40+
else
41+
path_array
42+
end
3343
end
3444

3545
attr_accessor :graphql_dead

lib/graphql/query.rb

Lines changed: 55 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,47 @@ class Query
1010
autoload :Context, "graphql/query/context"
1111
autoload :Fingerprint, "graphql/query/fingerprint"
1212
autoload :NullContext, "graphql/query/null_context"
13+
autoload :Partial, "graphql/query/partial"
1314
autoload :Result, "graphql/query/result"
1415
autoload :Variables, "graphql/query/variables"
1516
autoload :InputValidationResult, "graphql/query/input_validation_result"
1617
autoload :VariableValidationError, "graphql/query/variable_validation_error"
1718
autoload :ValidationPipeline, "graphql/query/validation_pipeline"
1819

20+
# Code shared with {Partial}
21+
module Runnable
22+
def after_lazy(value, &block)
23+
if !defined?(@runtime_instance)
24+
@runtime_instance = context.namespace(:interpreter_runtime)[:runtime]
25+
end
26+
27+
if @runtime_instance
28+
@runtime_instance.minimal_after_lazy(value, &block)
29+
else
30+
@schema.after_lazy(value, &block)
31+
end
32+
end
33+
34+
# Node-level cache for calculating arguments. Used during execution and query analysis.
35+
# @param ast_node [GraphQL::Language::Nodes::AbstractNode]
36+
# @param definition [GraphQL::Schema::Field]
37+
# @param parent_object [GraphQL::Schema::Object]
38+
# @return [Hash{Symbol => Object}]
39+
def arguments_for(ast_node, definition, parent_object: nil)
40+
arguments_cache.fetch(ast_node, definition, parent_object)
41+
end
42+
43+
def arguments_cache
44+
@arguments_cache ||= Execution::Interpreter::ArgumentsCache.new(self)
45+
end
46+
47+
# @api private
48+
def handle_or_reraise(err)
49+
@schema.handle_or_reraise(context, err)
50+
end
51+
end
52+
53+
include Runnable
1954
class OperationNameMissingError < GraphQL::ExecutionError
2055
def initialize(name)
2156
msg = if name.nil?
@@ -198,19 +233,10 @@ def subscription_update?
198233
# @return [GraphQL::Execution::Lookahead]
199234
def lookahead
200235
@lookahead ||= begin
201-
ast_node = selected_operation
202-
if ast_node.nil?
236+
if selected_operation.nil?
203237
GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
204238
else
205-
root_type = case ast_node.operation_type
206-
when nil, "query"
207-
types.query_root # rubocop:disable Development/ContextIsPassedCop
208-
when "mutation"
209-
types.mutation_root # rubocop:disable Development/ContextIsPassedCop
210-
when "subscription"
211-
types.subscription_root # rubocop:disable Development/ContextIsPassedCop
212-
end
213-
GraphQL::Execution::Lookahead.new(query: self, root_type: root_type, ast_nodes: [ast_node])
239+
GraphQL::Execution::Lookahead.new(query: self, root_type: root_type, ast_nodes: [selected_operation])
214240
end
215241
end
216242
end
@@ -236,6 +262,18 @@ def operations
236262
with_prepared_ast { @operations }
237263
end
238264

265+
# Run subtree partials of this query and return their results.
266+
# Each partial is identified with a `path:` and `object:`
267+
# where the path references a field in the AST and the object will be treated
268+
# as the return value from that field. Subfields of the field named by `path`
269+
# will be executed with `object` as the starting point
270+
# @param partials_hashes [Array<Hash{Symbol => Object}>] Hashes with `path:` and `object:` keys
271+
# @return [Array<GraphQL::Query::Result>]
272+
def run_partials(partials_hashes)
273+
partials = partials_hashes.map { |partial_options| Partial.new(query: self, **partial_options) }
274+
Execution::Interpreter.run_all(@schema, partials, context: @context)
275+
end
276+
239277
# Get the result for this query, executing it once
240278
# @return [GraphQL::Query::Result] A Hash-like GraphQL response, with `"data"` and/or `"errors"` keys
241279
def result
@@ -278,19 +316,6 @@ def variables
278316
end
279317
end
280318

281-
# Node-level cache for calculating arguments. Used during execution and query analysis.
282-
# @param ast_node [GraphQL::Language::Nodes::AbstractNode]
283-
# @param definition [GraphQL::Schema::Field]
284-
# @param parent_object [GraphQL::Schema::Object]
285-
# @return [Hash{Symbol => Object}]
286-
def arguments_for(ast_node, definition, parent_object: nil)
287-
arguments_cache.fetch(ast_node, definition, parent_object)
288-
end
289-
290-
def arguments_cache
291-
@arguments_cache ||= Execution::Interpreter::ArgumentsCache.new(self)
292-
end
293-
294319
# A version of the given query string, with:
295320
# - Variables inlined to the query
296321
# - Strings replaced with `<REDACTED>`
@@ -357,17 +382,21 @@ def possible_types(type)
357382

358383
def root_type_for_operation(op_type)
359384
case op_type
360-
when "query"
385+
when "query", nil
361386
types.query_root # rubocop:disable Development/ContextIsPassedCop
362387
when "mutation"
363388
types.mutation_root # rubocop:disable Development/ContextIsPassedCop
364389
when "subscription"
365390
types.subscription_root # rubocop:disable Development/ContextIsPassedCop
366391
else
367-
raise ArgumentError, "unexpected root type name: #{op_type.inspect}; expected 'query', 'mutation', or 'subscription'"
392+
raise ArgumentError, "unexpected root type name: #{op_type.inspect}; expected nil, 'query', 'mutation', or 'subscription'"
368393
end
369394
end
370395

396+
def root_type
397+
root_type_for_operation(selected_operation.operation_type)
398+
end
399+
371400
def types
372401
@visibility_profile || warden.visibility_profile
373402
end
@@ -400,23 +429,6 @@ def subscription?
400429
with_prepared_ast { @subscription }
401430
end
402431

403-
# @api private
404-
def handle_or_reraise(err)
405-
schema.handle_or_reraise(context, err)
406-
end
407-
408-
def after_lazy(value, &block)
409-
if !defined?(@runtime_instance)
410-
@runtime_instance = context.namespace(:interpreter_runtime)[:runtime]
411-
end
412-
413-
if @runtime_instance
414-
@runtime_instance.minimal_after_lazy(value, &block)
415-
else
416-
@schema.after_lazy(value, &block)
417-
end
418-
end
419-
420432
attr_reader :logger
421433

422434
private

0 commit comments

Comments
 (0)