Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support lazy enumerators for @stream #4920

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions lib/graphql/execution/interpreter/runtime.rb
Original file line number Diff line number Diff line change
Expand Up @@ -658,25 +658,21 @@ def continue_field(value, owner_type, field, current_type, ast_node, next_select
inner_type_non_null = inner_type.non_null?
response_list = GraphQLResultArray.new(result_name, selection_result, is_non_null)
set_result(selection_result, result_name, response_list, true, is_non_null)
idx = nil
idx = 0
any_items_resolved = false
list_value = begin
value.each do |inner_value|
idx ||= 0
this_idx = idx
idx += 1
if use_dataloader_job
@dataloader.append_job do
resolve_list_item(inner_value, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type, was_scoped, runtime_state)
end
else
resolve_list_item(inner_value, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type, was_scoped, runtime_state)
enumerator = value.to_enum
if use_dataloader_job
@dataloader.append_job do
resolve_list_item(enumerator, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, idx, response_list, next_selections, owner_type, was_scoped, runtime_state)
end
else
resolve_list_item(enumerator, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, idx, response_list, next_selections, owner_type, was_scoped, runtime_state)
end

response_list
rescue NoMethodError => err
# Ruby 2.2 doesn't have NoMethodError#receiver, can't check that one in this case. (It's been EOL since 2017.)
if err.name == :each && (err.respond_to?(:receiver) ? err.receiver == value : true)
if err.receiver == value && (err.name == :each || err.name == :to_enum)
# This happens when the GraphQL schema doesn't match the implementation. Help the dev debug.
raise ListResultFailedError.new(value: value, field: field, path: current_path)
else
Expand All @@ -693,24 +689,29 @@ def continue_field(value, owner_type, field, current_type, ast_node, next_select
end
end
# Detect whether this error came while calling `.each` (before `idx` is set) or while running list *items* (after `idx` is set)
error_is_non_null = idx.nil? ? is_non_null : inner_type.non_null?
error_is_non_null = get_current_runtime_state.current_result_name.is_a?(String) ? is_non_null : inner_type.non_null?
continue_value(list_value, owner_type, field, error_is_non_null, ast_node, result_name, selection_result)
else
raise "Invariant: Unhandled type kind #{current_type.kind} (#{current_type})"
end
end

def resolve_list_item(inner_value, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type, was_scoped, runtime_state) # rubocop:disable Metrics/ParameterLists
def resolve_list_item(enumerator, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type, was_scoped, runtime_state) # rubocop:disable Metrics/ParameterLists
runtime_state = get_current_runtime_state
runtime_state.current_result_name = this_idx
runtime_state.current_result = response_list
call_method_on_directives(:resolve_each, owner_object, ast_node.directives) do
inner_value = enumerator.next
# This will update `response_list` with the lazy
after_lazy(inner_value, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, result_name: this_idx, result: response_list, runtime_state: runtime_state) do |inner_inner_value, runtime_state|
continue_value = continue_value(inner_inner_value, owner_type, field, inner_type_non_null, ast_node, this_idx, response_list)
if HALT != continue_value
continue_field(continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments, this_idx, response_list, was_scoped, runtime_state)
end
resolve_list_item(enumerator, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx + 1, response_list, next_selections, owner_type, was_scoped, runtime_state)
end
rescue StopIteration
nil
end
end

Expand Down
2 changes: 1 addition & 1 deletion spec/graphql/pagination/connections_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def things2
pp ConnectionErrorTestSchema.execute("{ things { name } }")
end

assert_includes err.message, "Failed to build a GraphQL list result for field `Query.things` at path `things`."
assert_includes err.message, "Failed to build a GraphQL list result for field `Query.things` at path `things.0`."
assert_includes err.message, "(GraphQL::Pagination::ArrayConnection) to implement `.each` to satisfy the GraphQL return type `[ThingConnection!]!`"
assert_includes err.message, "This field was treated as a Relay-style connection; add `connection: false` to the `field(...)` to disable this behavior."
end
Expand Down
53 changes: 53 additions & 0 deletions spec/graphql/schema/list_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,57 @@ def items(ids:)
assert_equal 3, res["errors"][0]["extensions"]["problems"].count
end
end

describe "Lazy Enumerators" do
class LazyEnumeratorSchema < GraphQL::Schema
class LazyItems
def initialize(log)
@log = log
end

def each
@log << "yield 1"
yield(1)
@log << "yield 2"
yield(2)
@log << "yield 3"
yield(3)
self
end
end

class Item < GraphQL::Schema::Object
field :name, String

def name
context[:list_log] << "resolve #{object}.name"
"name-#{object}"
end
end
class Query < GraphQL::Schema::Object
field :items, [Item]

def items
LazyItems.new(context[:list_log])
end
end

query(Query)
end

it "resolves them lazily" do
query_str = "{ items { name } }"
log = []
LazyEnumeratorSchema.execute(query_str, context: { list_log: log })
expected_log = [
"yield 1",
"resolve 1.name",
"yield 2",
"resolve 2.name",
"yield 3",
"resolve 3.name"
]
assert_equal expected_log, log
end
end
end
Loading