Module: Pipeloader::Batch::Model::ClassMethods

Defined in:
lib/pipeloader/batch/model.rb

Constant Summary collapse

AGGREGATE_DEFAULTS =

Sentinel marking “no explicit default given” so we can pick a sensible per-function default (0 for count/sum, nil for min/max/average).

{ count: 0, sum: 0 }.freeze

Instance Method Summary collapse

Instance Method Details

#_pipeloader_batch_association(name) ⇒ Object



124
125
126
127
# File 'lib/pipeloader/batch/model.rb', line 124

def _pipeloader_batch_association(name)
  # merge (not mutate) so subclasses get their own copy — STI-safe.
  self._pipeloader_batch_associations = _pipeloader_batch_associations + [name]
end

#_pipeloader_batch_register_aggregate(name, relationship) ⇒ Object



129
130
131
# File 'lib/pipeloader/batch/model.rb', line 129

def _pipeloader_batch_register_aggregate(name, relationship)
  _pipeloader_batch_register_fetcher(name, relationship.getter, relationship.loader, relationship.default)
end

#_pipeloader_batch_register_fetcher(name, getter, loader, default) ⇒ Object



133
134
135
136
137
138
# File 'lib/pipeloader/batch/model.rb', line 133

def _pipeloader_batch_register_fetcher(name, getter, loader, default)
  self._pipeloader_batch_fetchers = _pipeloader_batch_fetchers.merge(
    name => Pipeloader::Batch::Fetcher.new(self, name, getter, loader, default: default)
  )
  define_method(name) { batch_load(name) }
end

#batch(name, key: nil, default: nil, &loader) ⇒ Object

Define ‘name` as a value loaded ONCE across every live sibling. The loader gets the array of owner keys (the `key:` column, default the primary key) and returns a `{ key => value }` Hash; each instance reads its own value, or `default` when its key is absent. This is the escape hatch behind batch_count / batch_aggregate, surfaced for everything that isn’t a plain association — existence checks, viewer-scoped flags, lookups by a non-PK column, derived rows:

batch :viewer_has_starred, default: false do |repo_ids|
  Star.where(user_id: Current.user.id, repo_id: repo_ids)
      .pluck(:repo_id).index_with(true)         # missing -> false
end


117
118
119
120
121
122
# File 'lib/pipeloader/batch/model.rb', line 117

def batch(name, key: nil, default: nil, &loader)
  raise Pipeloader::Batch::Error, "batch #{name.inspect} requires a loader block" unless loader

  getter = (key || primary_key).to_sym
  _pipeloader_batch_register_fetcher(name, getter, ->(keys, _fetcher) { loader.call(keys) }, default)
end

#batch_aggregate(name, function:, of: nil, column: nil, class_name: nil, foreign_key: nil, primary_key: nil, default: AGGREGATE_DEFAULTS) ⇒ Object

function: :sum / :average / :minimum / :maximum (or :count). Empty groups default to 0 for count/sum and nil for the rest, override with :default. For anything a plain GROUP BY can’t express, use ‘batch` with a loader.



88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/pipeloader/batch/model.rb', line 88

def batch_aggregate(name, function:, of: nil, column: nil, class_name: nil, foreign_key: nil,
                    primary_key: nil, default: AGGREGATE_DEFAULTS)
  if function != :count && column.nil?
    raise Pipeloader::Batch::Error, "batch_aggregate #{name.inspect} (#{function}) requires a :column"
  end

  resolved_default = default.equal?(AGGREGATE_DEFAULTS) ? AGGREGATE_DEFAULTS[function] : default
  _pipeloader_batch_register_aggregate(name, Relationship.aggregate(
    self, name,
    of: of, function: function, column: column,
    class_name: class_name, foreign_key: foreign_key, primary_key: primary_key,
    default: resolved_default
  ))
end

#batch_belongs_to(name, scope = nil, **options, &extension) ⇒ Object



65
66
67
68
# File 'lib/pipeloader/batch/model.rb', line 65

def batch_belongs_to(name, scope = nil, **options, &extension)
  belongs_to(name, scope, **options, &extension)
  _pipeloader_batch_association(name)
end

#batch_count(name, of: nil, class_name: nil, foreign_key: nil, primary_key: nil, default: 0) ⇒ Object

— Scalar aggregates: COUNT / SUM / AVERAGE / MINIMUM / MAXIMUM —



76
77
78
79
80
81
82
83
# File 'lib/pipeloader/batch/model.rb', line 76

def batch_count(name, of: nil, class_name: nil, foreign_key: nil, primary_key: nil, default: 0)
  _pipeloader_batch_register_aggregate(name, Relationship.aggregate(
    self, name,
    of: of, function: :count, column: nil,
    class_name: class_name, foreign_key: foreign_key, primary_key: primary_key,
    default: default
  ))
end

#batch_has_many(name, scope = nil, **options, &extension) ⇒ Object

— Collection: a real has_many, but the reader returns a BatchProxy —



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/pipeloader/batch/model.rb', line 40

def batch_has_many(name, scope = nil, **options, &extension)
  has_many(name, scope, **options, &extension)
  reflection = reflect_on_association(name)
  if reflection.through_reflection
    # A :through collection's target has no direct FK to the owner, so the
    # flat-FK BatchProxy can't build its query. Batch it through AR's
    # Preloader (which walks the join), filling every live sibling at once;
    # the reader returns a plain loaded array (no chainable proxy).
    define_method(name) do
      assoc = association(name)
      Pipeloader::Batch.fill_association(self, name) unless assoc.loaded?
      assoc.load_target
    end
  else
    define_method(name) { Pipeloader::Batch::BatchProxy.new(self, reflection) }
  end
end

#batch_has_one(name, scope = nil, **options, &extension) ⇒ Object

— Singular: a real association whose load is batched (single record) —



60
61
62
63
# File 'lib/pipeloader/batch/model.rb', line 60

def batch_has_one(name, scope = nil, **options, &extension)
  has_one(name, scope, **options, &extension)
  _pipeloader_batch_association(name)
end

#pipeloader_batched_association?(name) ⇒ Boolean

Returns:

  • (Boolean)


70
71
72
# File 'lib/pipeloader/batch/model.rb', line 70

def pipeloader_batched_association?(name)
  _pipeloader_batch_associations.include?(name)
end