rmosolgo / rmosolgo/graphql-ruby

QueryComplexity analyzer may have IO side-effect

Open
#5,261 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Ruby
Stars
5.4k
Forks
1.4k
Avg merge
23h 19m
Merged PRs (30d)
28

Description

Describe the bug

Discussed in https://github.com/rmosolgo/graphql-ruby/pull/4800#issuecomment-2684939962

The GraphQL::Analysis::QueryComplexity may have IO side-effects (eg making a DB query via active record), while it would be expected to be fully static (as mentioned in the doc "None of GraphQL-Ruby’s validators make IO calls").
This happens because the field complexity calculation for connections (in https://github.com/rmosolgo/graphql-ruby/blob/v2.4.10/lib/graphql/schema/field.rb#L504) calls the argument_cache to read the arguments (to read static arguments like :first), which fully resolves the arguments. This is a problem for example when an argument uses loads: to get an ActiveRecord object from id. This makes using the validate_timeout feature for analysis unsafe.

Versions

graphql version: 2.4.10

Script to reproduce

(took inspiration from the script in https://github.com/rmosolgo/graphql-ruby/issues/5036#issuecomment-2298473769)

require 'bundler/inline'

gemfile do
  gem 'graphql'
  gem 'sqlite3'
  gem 'activerecord', require: 'active_record'
end

ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
ActiveRecord::Schema.define do
  create_table :mentions do |t|
    t.string(:content)
    t.belongs_to :ticket
  end
  create_table :tickets do |t|
    t.string(:subject)
  end
end

class Mention < ActiveRecord::Base; end

class Ticket < ActiveRecord::Base
  has_many :mentions
end

ticket = Ticket.create!(subject: "Ticket 1")

Mention.create!(content: "Mention 1", ticket_id: ticket.id)

class MySchema < GraphQL::Schema

  class Mention < GraphQL::Schema::Object
    field :content, String
  end

  class Ticket < GraphQL::Schema::Object
    field :subject, String
    field :mentions, Mention.connection_type

    def mentions(obj)
      obj.mentions
    end
  end

  class Query < GraphQL::Schema::Object
    field :mentions, Mention.connection_type do
      argument :ticket_id, ID, loads: Ticket
    end

    def mentions(ticket:)
      ticket.mentions
    end
  end

  default_page_size 50
  max_complexity 1000
  query(Query)

  def self.object_from_id(id, ctx)
    ::Ticket.find(id)
  end

  def self.resolve_type(abs_t, obj, ctx)
    Ticket
  end
end

query_str = <<~GRAPHQL
  query queryOne($ticketId: ID!, $first: Int!) {
    mentions(ticketId: $ticketId, first: $first) {
      nodes { content }
    }
  }
GRAPHQL

ActiveRecord::Base.logger = Logger.new(STDOUT) # Logs ActiveRecord SQL queries

query = GraphQL::Query.new(MySchema, query_str, variables: { ticketId: '1', first: 10 })

pp GraphQL::Analysis.analyze_query(query, [GraphQL::Analysis::QueryComplexity])
# D, [2025-02-28T16:55:50.009374 #66030] DEBUG -- :   Ticket Load (0.2ms)  SELECT "tickets".* FROM "tickets" WHERE "tickets"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
# [12]
pp query.arguments_cache.instance_variable_get(:@storage).values[0].values[0].values[0]
# #<GraphQL::Execution::Interpreter::Arguments @keyword_arguments={:first=>10, :ticket=>#<Ticket id: 1, subject: "Ticket 1">}>

Expected behavior

No DB call should be made, analyzer should be fully static. Arguments that are needed for the complexity calculation should be read statically.

Actual behavior

We can see a DB call made through ActiveRecord

D, [2025-02-28T16:55:50.009374 #66030] DEBUG -- :   Ticket Load (0.2ms)  SELECT "tickets".* FROM "tickets" WHERE "tickets"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]

And can see the argument resolved in the arguments_cache

 #<GraphQL::Execution::Interpreter::Arguments @keyword_arguments={:first=>10, :ticket=>#<Ticket id: 1, subject: "Ticket 1">}>

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with lib/graphql/schema/field.rb around the connection complexity calculation and the GraphQL::Analysis::QueryComplexity entry point. Run the reproduction script with ActiveRecord logging enabled, then trace how argument_cache is accessed during analysis. Done means complexity analysis reads needed arguments statically without triggering the loads: lookup or another database call.

Written by the indexing model from the issue text.

Assessment

Tech stack
ruby
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.