Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e2eff74
Migrate to RuntimeError for error checks, add Finalizers API
rmosolgo Apr 1, 2026
f0ca079
migrate RawValue to use finalizer
rmosolgo Apr 1, 2026
d77333c
Update docs
rmosolgo Apr 1, 2026
f5ae21a
Support finalizers which don't appear in the result; add some directi…
rmosolgo Apr 3, 2026
ed0fb5e
Improve directives, make optimizations
rmosolgo Apr 7, 2026
316392d
revert some optimizations
rmosolgo Apr 7, 2026
f5453e8
Try postprocessors
rmosolgo Apr 10, 2026
43edf31
Migrate finalizers to use a map of value -> f
rmosolgo Apr 13, 2026
c71d414
Remove incorrect nil check, fix syntax for old ruby
rmosolgo Apr 13, 2026
c489d92
Move paths to instance state, fix test
rmosolgo Apr 13, 2026
cf62bee
Update test for exec-next
rmosolgo Apr 13, 2026
51051d0
Keep making partials work
rmosolgo Apr 13, 2026
fa45902
Fix lint errors, get more partial specs working
rmosolgo Apr 14, 2026
259be99
Fix a couple more partial specs
rmosolgo Apr 14, 2026
61a0013
Fix finalizing query.context.errors, more partial fixes
rmosolgo Apr 14, 2026
2f2ca57
Fix lint error
rmosolgo Apr 14, 2026
85c8dfc
build finalizer hashes without default procs
rmosolgo Apr 15, 2026
45e39dc
exit finalize when all finalizers have run
rmosolgo Apr 15, 2026
187c32d
Move subscription cleanup to finalizer
rmosolgo Apr 15, 2026
337fd3c
reapply trace hooks
rmosolgo Apr 15, 2026
067ab9c
Add dedicated object-related hooks
rmosolgo Apr 16, 2026
8d4b5ea
Fix tracing and objects in partials on abstract type
rmosolgo Apr 16, 2026
fe20a13
fix lit
rmosolgo Apr 16, 2026
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
6 changes: 5 additions & 1 deletion cop/development/trace_methods_cop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class TraceMethodsCop < RuboCop::Cop::Base
:execute_query,
:execute_query_lazy,
:lex,
:object_loaded,
:objects,
:parse,
:resolve_type,
:resolve_type_lazy,
Expand Down Expand Up @@ -70,7 +72,9 @@ def on_module(node)
:lex, :analyze_query, :execute_query, :execute_query_lazy,
# Only useful for isolated event tracking:
:begin_dataloader, :end_dataloader,
:dataloader_fiber_exit, :dataloader_spawn_execution_fiber, :dataloader_spawn_source_fiber
:dataloader_fiber_exit, :dataloader_spawn_execution_fiber, :dataloader_spawn_source_fiber,
# Tracks object references, but not durations:
:objects, :object_loaded
]
missing_defs.each do |missing_def|
if all_defs.include?(:"begin_#{missing_def}") && all_defs.include?(:"end_#{missing_def}")
Expand Down
3 changes: 3 additions & 0 deletions guides/_plugins/api_doc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,12 @@ def render(context)
end
text = h.gsub(/^#+ /, "")
target = text.downcase
.gsub("🟡", "00emoji00")
.gsub("❌", "00emoji00")
.gsub(/[^a-z0-9_]+/, "-")
.sub(/-$/, "")
.sub(/^-/, "")
.gsub("-00emoji00", "-")

rendered_text = Kramdown::Document.new(text, auto_ids: false)
.to_html
Expand Down
25 changes: 20 additions & 5 deletions guides/execution/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,23 @@ This is not supported because the new runtime doesn't actually produce `current_

It is theoretically possible to support this but it will be a ton of work. If you use this for core runtime functions, please share your use case in a GitHub issue and we can investigate future options.

### Scoped context ❌

This is currently implemented with `current_path`. Another implementation is probably possible but not implemented yet. Please open an issue to discuss.

### `@defer` and `@stream` ❌

This depends on `current_path` so isn't possible yet.
`@defer` is supported with an implementation difference that _probably_ doesn't affect your application: previously, `@defer` worked by pausing and resuming the _same `GraphQL::Query` instance_. However, with `Execution::Next`, `@defer` takes a different approach. Instead, when a `GraphQL::Query` encounters `@defer`, it notes the location in the document and stops executing that branch. Later, when you request the deferred result, that branch of the query is resumed using a new instance of `GraphQL::Query::Partial`.

This might matter if you're modifying `context` at runtime because those new instances _also_ have fresh `Query::Context` instances. The original query context _will_ get copied into the `@defer` branches using `Query::Context.new(**original_query.context.to_h)`, so any custom values will be available. But if you _assign new keys_ after the context is copied, those keys won't appear when running later `@defer`ed branches.

To handle this, you can refactor how you accumulate data during execution. Instead of `||=`'ing into `context[...]` during execution, assign a new accumulator object _before_ starting the query, then call methods on that object to make any necessary state changes. That new object _will_ be copied into `@defer` partials, and since the object is shared between the different branches, any necessary state changes will still be "seen" everywhere.

If this gives you trouble, please feel free to email me or open an issue on GitHub to discuss a migration strategy.

##### GraphQL-Batch support

When using `Execution::Next`, no custom code is required to support `graphql-batch`.

### ObjectCache ❌

Expand Down Expand Up @@ -242,12 +256,13 @@ Resolver classes are called.

### `raw_value` 🟡

Supported but requires a manual opt-in at schema level. Support for this will probably get better somehow in a future version.
Supported, but the `raw_value` call must be made on `context`, for example:

```ruby
class MyAppSchema < GraphQL::Schema
uses_raw_value(true) # TODO This configuration will be improved in a future GraphQL-Ruby version
use GraphQL::Execution::Next
field :values, SomeObjectType, resolve_static: true

def self.values(context)
context.raw_value(...)
end
```

Expand Down
4 changes: 1 addition & 3 deletions lib/graphql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ def self.eager_load!
class Error < StandardError
end

class RuntimeError < Error
end

# This error is raised when GraphQL-Ruby encounters a situation
# that it *thought* would never happen. Please report this bug!
class InvariantError < Error
Expand Down Expand Up @@ -122,6 +119,7 @@ class << self
autoload :ParseError, "graphql/parse_error"
autoload :Backtrace, "graphql/backtrace"

autoload :RuntimeError, "graphql/runtime_error"
autoload :UnauthorizedError, "graphql/unauthorized_error"
autoload :UnauthorizedEnumValueError, "graphql/unauthorized_enum_value_error"
autoload :UnauthorizedFieldError, "graphql/unauthorized_field_error"
Expand Down
33 changes: 20 additions & 13 deletions lib/graphql/analysis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ def analyze_multiplex(multiplex, analyzers)
end
end

multiplex_results = multiplex_analyzers.map(&:result)
multiplex_errors = analysis_errors(multiplex_results)

multiplex_analyzers.map!(&:result)
multiplex_errors = analysis_errors(EmptyObjects::EMPTY_ARRAY, multiplex_analyzers)
multiplex.queries.each_with_index do |query, idx|
query.analysis_errors = multiplex_errors + analysis_errors(query_results[idx])
query.analysis_errors = analysis_errors(multiplex_errors, query_results[idx])
end
multiplex_results
multiplex_analyzers
end
end

Expand All @@ -55,13 +55,11 @@ def analyze_multiplex(multiplex, analyzers)
# @return [Array<Any>] Results from those analyzers
def analyze_query(query, analyzers, multiplex_analyzers: [])
query.current_trace.analyze_query(query: query) do
query_analyzers = analyzers
.map { |analyzer| analyzer.new(query) }
.tap { _1.select!(&:analyze?) }

query_analyzers = analyzers.map { |analyzer| analyzer.new(query) }
query_analyzers.select!(&:analyze?)
analyzers_to_run = query_analyzers + multiplex_analyzers
if !analyzers_to_run.empty?

if !analyzers_to_run.empty?
analyzers_to_run.select!(&:visit?)
if !analyzers_to_run.empty?
visitor = GraphQL::Analysis::Visitor.new(
Expand All @@ -79,18 +77,27 @@ def analyze_query(query, analyzers, multiplex_analyzers: [])

query_analyzers.map(&:result)
else
[]
EmptyObjects::EMPTY_ARRAY
end
end
rescue TimeoutError => err
[err]
rescue GraphQL::UnauthorizedError, GraphQL::ExecutionError
# This error was raised during analysis and will be returned the client before execution
[]
EmptyObjects::EMPTY_ARRAY
end

def analysis_errors(results)
results.flatten.tap { _1.select! { |r| r.is_a?(GraphQL::AnalysisError) } }
def analysis_errors(parent_errors, results)
if !results.empty?
results = results.flatten
results.select! { |r| r.is_a?(GraphQL::AnalysisError) }
end

if parent_errors.empty?
results
else
parent_errors + results
end
end
end
end
4 changes: 2 additions & 2 deletions lib/graphql/execution.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# frozen_string_literal: true
require "graphql/execution/directive_checks"
require "graphql/execution/next"
require "graphql/execution/interpreter"
require "graphql/execution/lazy"
require "graphql/execution/lookahead"
require "graphql/execution/multiplex"
require "graphql/execution/next"
require "graphql/execution/errors"

module GraphQL
Expand All @@ -14,7 +14,7 @@ class Skip < GraphQL::RuntimeError
attr_accessor :path
def ast_nodes=(_ignored); end

def assign_graphql_result(query, result_data, key)
def finalize_graphql_result(query, result_data, key)
result_data.delete(key)
end
end
Expand Down
6 changes: 6 additions & 0 deletions lib/graphql/execution/interpreter/handles_raw_value.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ module Execution
class Interpreter
# Wrapper for raw values
class RawValue
include GraphQL::Execution::Next::Finalizer

def finalize_graphql_result(query, result_data, result_key)
result_data[result_key] = @object
end

def initialize(obj = nil)
@object = obj
end
Expand Down
17 changes: 17 additions & 0 deletions lib/graphql/execution/next.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "graphql/execution/next/prepare_object_step"
require "graphql/execution/next/field_resolve_step"
require "graphql/execution/next/finalize"
require "graphql/execution/next/load_argument_step"
require "graphql/execution/next/runner"
require "graphql/execution/next/selections_step"
Expand Down Expand Up @@ -67,6 +68,22 @@ def self.run_all(schema, query_options, context: {}, max_complexity: schema.max_
runner = Runner.new(multiplex, **schema.execution_next_options)
runner.execute
end

module Finalizer
attr_accessor :path
def finalize_graphql_result(query, result_data, result_key)
raise RequiredImplementationMissingError
end
end

module HaltExecution
end

module PostProcessor
def after_resolve(field_results)
raise RequiredImplementationMissingError, "#{self.class}#after_resolve should handle `field_results` and return a new value to use"
end
end
end
end
end
Loading
Loading