Class: Pipeloader::Batch::Relationship

Inherits:
Object
  • Object
show all
Defined in:
lib/pipeloader/batch/relationship.rb

Overview

Builds the pieces an aggregate Fetcher needs: a ‘getter` (the owner key read off each sibling), a `loader` lambda (one GROUP BY query returning a { key => scalar } hash), and a `default` for keys with no rows.

The child class and keys are resolved with this precedence:

explicit option > AR reflection (if the source names a real association) > convention.

The target class constant is resolved lazily so autoload order doesn’t matter.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(getter, loader, default) ⇒ Relationship

Returns a new instance of Relationship.



61
62
63
64
65
# File 'lib/pipeloader/batch/relationship.rb', line 61

def initialize(getter, loader, default)
  @getter = getter
  @loader = loader
  @default = default
end

Instance Attribute Details

#defaultObject (readonly)

Returns the value of attribute default.



15
16
17
# File 'lib/pipeloader/batch/relationship.rb', line 15

def default
  @default
end

#getterObject (readonly)

Returns the value of attribute getter.



15
16
17
# File 'lib/pipeloader/batch/relationship.rb', line 15

def getter
  @getter
end

#loaderObject (readonly)

Returns the value of attribute loader.



15
16
17
# File 'lib/pipeloader/batch/relationship.rb', line 15

def loader
  @loader
end

Class Method Details

.aggregate(owner, name, of:, function:, column:, class_name:, foreign_key:, primary_key:, default:) ⇒ Object



17
18
19
20
21
22
23
24
25
26
# File 'lib/pipeloader/batch/relationship.rb', line 17

def self.aggregate(owner, name, of:, function:, column:, class_name:, foreign_key:, primary_key:, default:)
  source = of || default_source(name, function)
  resolve, fk, getter = child_keys(owner, source, class_name, foreign_key, primary_key)

  derived = lambda do |ids, _fetcher|
    scope = resolve.call.where(fk => ids).group(fk)
    function == :count ? scope.count : scope.public_send(function, column)
  end
  new(getter, derived, default)
end

.child_keys(owner, assoc_name, class_name, foreign_key, primary_key) ⇒ Object

Resolve [class-resolver, foreign-key, owner-getter] for a has_many-style source rooted at ‘assoc_name` on `owner`.



30
31
32
33
34
35
36
# File 'lib/pipeloader/batch/relationship.rb', line 30

def self.child_keys(owner, assoc_name, class_name, foreign_key, primary_key)
  reflection = owner.reflect_on_association(assoc_name)
  fk = (foreign_key || reflection&.foreign_key || owner.model_name.element.foreign_key).to_s
  getter = (primary_key || reflection&.active_record_primary_key || owner.primary_key).to_sym
  resolve = class_resolver(class_name, reflection) { assoc_name.to_s.singularize.camelize.constantize }
  [resolve, fk, getter]
end

.class_resolver(class_name, reflection, &convention) ⇒ Object

Memoized lambda producing the target class constant. The convention block is only used when neither class_name nor a reflection is available.



47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/pipeloader/batch/relationship.rb', line 47

def self.class_resolver(class_name, reflection, &convention)
  resolved = nil
  lambda do
    resolved ||=
      if class_name
        class_name.to_s.constantize
      elsif reflection
        reflection.klass
      else
        convention.call
      end
  end
end

.default_source(name, function) ⇒ Object

For ‘batch_count :books_count`, derive the source association (“books”) by stripping the function suffix when present.



40
41
42
43
# File 'lib/pipeloader/batch/relationship.rb', line 40

def self.default_source(name, function)
  suffix = "_#{function}"
  name.to_s.end_with?(suffix) ? name.to_s.delete_suffix(suffix) : name.to_s
end