Describe the bug
Schema.from_definition fails to populate possible_types for ancestor interfaces when a child interface appears before its parent interface in the SDL document. The parent becomes a LateBoundType placeholder. When it is later resolved, the child interface's concrete possible_types are not propagated up to the grandparent.
This causes to_definition to silently drop the affected interfaces (and any fields referencing them) because the Warden considers interfaces with no possible_types to be invisible.
Versions
graphql version: 2.5.21
rails (or other framework): N/A
GraphQL schema
3-interface hierarchy (Grandparent < Parent < Child) plus a concrete type Leaf. Each type lists only its direct parent in implements. Child appears before Parent:
type Query { grandparent: Grandparent }
interface Grandparent { id: ID! }
interface Child implements Parent { id: ID! }
type Leaf implements Child { id: ID! }
interface Parent implements Grandparent { id: ID! }
Steps to reproduce
Save the reproduction script below and run ruby graphql_ruby_bug_repro.rb. It tests three variants of the same hierarchy:
- BUG:
Child defiend before Parent → possible_types(Grandparent) is empty
- WORKAROUND: all transitive interfaces listed explicitly → works
- CONTROL: parents defined before children (no LateBoundType) → works
Output:
graphql gem version: 2.5.21
BUG: child interface defined before parent
possible_types(Child): ["Leaf"] OK
possible_types(Grandparent): [] BUG
possible_types(Parent): ["Leaf"] OK
to_definition: DROPPED: Grandparent, Parent, Child
WORKAROUND: all transitive interfaces listed
possible_types(Child): ["Leaf"] OK
possible_types(Grandparent): ["Leaf"] OK
possible_types(Parent): ["Leaf"] OK
to_definition: all interfaces visible
CONTROL: parents defined before children
possible_types(Child): ["Leaf"] OK
possible_types(Grandparent): ["Leaf"] OK
possible_types(Parent): ["Leaf"] OK
to_definition: all interfaces visible
Full reproduction script
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "graphql", ENV.fetch("GRAPHQL_GEM_VERSION", "2.5.21")
end
require "graphql"
cases = {
"BUG: child interface defined before parent" => <<~GRAPHQL,
type Query { grandparent: Grandparent }
interface Grandparent { id: ID! }
interface Child implements Parent { id: ID! }
type Leaf implements Child { id: ID! }
interface Parent implements Grandparent { id: ID! }
GRAPHQL
"WORKAROUND: all transitive interfaces listed" => <<~GRAPHQL,
type Query { grandparent: Grandparent }
interface Grandparent { id: ID! }
interface Child implements Parent & Grandparent { id: ID! }
type Leaf implements Child & Parent & Grandparent { id: ID! }
interface Parent implements Grandparent { id: ID! }
GRAPHQL
"CONTROL: parents defined before children" => <<~GRAPHQL
type Query { grandparent: Grandparent }
interface Grandparent { id: ID! }
interface Parent implements Grandparent { id: ID! }
interface Child implements Parent { id: ID! }
type Leaf implements Child { id: ID! }
GRAPHQL
}
puts "graphql gem version: #{GraphQL::VERSION}"
puts
cases.each do |label, sdl|
schema = GraphQL::Schema.from_definition(sdl)
interfaces = schema.types.values.select { |t| t.kind.interface? && t.graphql_name != "Node" }
puts label
interfaces.sort_by(&:graphql_name).each do |iface|
possible = schema.possible_types(iface).map(&:graphql_name)
ok = possible.include?("Leaf")
puts " possible_types(#{iface.graphql_name}): #{possible.inspect} #{ok ? 'OK' : 'BUG'}"
end
round_tripped = schema.to_definition
missing = interfaces.reject { |iface| round_tripped.include?("interface #{iface.graphql_name}") }
puts " to_definition: #{missing.empty? ? 'all interfaces visible' : "DROPPED: #{missing.map(&:graphql_name).join(', ')}"}"
puts
end
Expected behavior
All three interfaces should include Leaf in possible_types, regardless of the order types appear in the SDL document.
Actual behavior
possible_types(Grandparent) is empty when Child appears before Parent. This causes Grandparent to become invisible in to_definition, which cascades — since Query.grandparent references it, that field is dropped too, making Parent and Child also unreachable.
I believe the bug is in Schema::Addition#update_type_owner. When a LateBoundType is resolved and the types waiting for it include interfaces (not just concrete types), the interfaces' concrete possible_types are not propagated to the newly resolved type's ancestors.
Workaround
Listing all transitive interfaces explicitly in every implements clause (e.g. type Leaf implements Child & Parent & Grandparent) avoids the bug. This also lines up with the GraphQL Spec. Arguably, graphql-ruby should maybe raise an exception when that is not done.
Additional context
Discovered in ElasticGraph (discussion #1075), where generated SDL had alphabetical type ordering and implements clauses listing only direct parents. Interface Edge types were losing their node fields after a from_definition/to_definition round-trip.
Describe the bug
Schema.from_definitionfails to populatepossible_typesfor ancestor interfaces when a child interface appears before its parent interface in the SDL document. The parent becomes aLateBoundTypeplaceholder. When it is later resolved, the child interface's concretepossible_typesare not propagated up to the grandparent.This causes
to_definitionto silently drop the affected interfaces (and any fields referencing them) because the Warden considers interfaces with nopossible_typesto be invisible.Versions
graphqlversion: 2.5.21rails(or other framework): N/AGraphQL schema
3-interface hierarchy (
Grandparent < Parent < Child) plus a concrete typeLeaf. Each type lists only its direct parent inimplements.Childappears beforeParent:Steps to reproduce
Save the reproduction script below and run
ruby graphql_ruby_bug_repro.rb. It tests three variants of the same hierarchy:Childdefiend beforeParent→possible_types(Grandparent)is emptyOutput:
Full reproduction script
Expected behavior
All three interfaces should include
Leafinpossible_types, regardless of the order types appear in the SDL document.Actual behavior
possible_types(Grandparent)is empty whenChildappears beforeParent. This causesGrandparentto become invisible into_definition, which cascades — sinceQuery.grandparentreferences it, that field is dropped too, makingParentandChildalso unreachable.I believe the bug is in
Schema::Addition#update_type_owner. When aLateBoundTypeis resolved and the types waiting for it include interfaces (not just concrete types), the interfaces' concretepossible_typesare not propagated to the newly resolved type's ancestors.Workaround
Listing all transitive interfaces explicitly in every
implementsclause (e.g.type Leaf implements Child & Parent & Grandparent) avoids the bug. This also lines up with the GraphQL Spec. Arguably,graphql-rubyshould maybe raise an exception when that is not done.Additional context
Discovered in ElasticGraph (discussion #1075), where generated SDL had alphabetical type ordering and
implementsclauses listing only direct parents. Interface Edge types were losing theirnodefields after afrom_definition/to_definitionround-trip.