Class: Pipeloader::ProjectionExtension

Inherits:
GraphQL::Schema::FieldExtension
  • Object
show all
Defined in:
lib/pipeloader/field_exact.rb

Overview

Narrows a returned ActiveRecord::Relation to exactly the columns the query needs — and bails to a whole-row fetch the moment a selected field can’t be proven to read only known columns, so it can never raise MissingAttributeError.

Instance Method Summary collapse

Instance Method Details

#resolve(object:, arguments:, context:) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/pipeloader/field_exact.rb', line 50

def resolve(object:, arguments:, context:, **)
  lookahead = arguments[:lookahead]
  inner = arguments.key?(:lookahead) ? arguments.reject { |k, _| k == :lookahead } : arguments

  # belongs_to and has_one are singular associations AR loads whole-row via
  # `object.assoc`; resolve them with a projected (and, when safe, fused) query
  # instead. Skipped if the type defines a custom resolver, or the selection is
  # opaque (then fall through to default).
  record = object.respond_to?(:object) ? object.object : object
  if lookahead && record.is_a?(ActiveRecord::Base) &&
     !field.owner.instance_methods(false).include?(field.resolver_method) &&
     (assoc = record.class.reflect_on_association(field.method_str.to_sym))
    if assoc.belongs_to?
      fk = record.public_send(assoc.foreign_key)
      return nil if fk.nil?

      cols = Pipeloader.project_columns(assoc.klass, lookahead)
      if cols
        # Mechanical batch: gather the level's foreign keys and resolve them with
        # one `WHERE pk = ANY($1)` instead of a query per parent, when demux is
        # provably unambiguous (see fusable_belongs_to?). The fused query is itself
        # pipelined, so round-trips stay = tree depth.
        if Pipeloader.fusable_belongs_to?(assoc)
          return Pipeloader.fuse(context.dataloader, assoc.klass, :by_pk, assoc.klass.primary_key, cols.sort, fk)
        end
        return assoc.klass.where(assoc.klass.primary_key => fk).select(*cols).first
      end
    elsif assoc.macro == :has_one && assoc.scope.nil? && assoc.through_reflection.nil?
      parent_key = record.public_send(assoc.active_record_primary_key)
      return nil if parent_key.nil?

      cols = Pipeloader.project_columns(assoc.klass, lookahead)
      if cols
        cols = (cols + [assoc.foreign_key]).uniq
        # has_one is the has_many query with a single-row demux. Fusing it is only
        # unambiguous when a unique index on the FK enforces 1:1 — otherwise the
        # ANY-scan's "first" could differ from the per-parent LIMIT 1.
        if Pipeloader.fusable_has_one?(assoc)
          return Pipeloader.fuse(context.dataloader, assoc.klass, :by_fk_one, assoc.foreign_key, cols.sort, parent_key)
        end
        return assoc.klass.where(assoc.foreign_key => parent_key).select(*cols).first
      end
    end
  end

  value = yield(object, inner)
  return value unless lookahead && value.is_a?(ActiveRecord::Relation)

  cols = Pipeloader.project_columns(value.klass, lookahead)
  return value unless cols # opaque field selected -> fetch whole rows

  proxy = value.respond_to?(:proxy_association) ? value.proxy_association : nil
  if proxy && Pipeloader.fusable_has_many?(proxy.reflection, value)
    # Mechanical batch: gather the level's parent keys and load all children
    # with one `WHERE fk IN (...)`, grouped back by foreign key. Safe only for a
    # plain has_many (no scope/limit), so each child row belongs to one parent.
    refl = proxy.reflection
    cols = (cols + [refl.foreign_key]).uniq
    parent_key = proxy.owner.public_send(refl.active_record_primary_key)
    return Pipeloader.fuse(context.dataloader, refl.klass, :by_fk_many, refl.foreign_key, cols.sort, parent_key)
  end

  # Keep a has_many's foreign key so AR can still group / wire the inverse.
  cols += Array(proxy.reflection.foreign_key) if proxy
  value.select(*cols.uniq)
end