Class: PaperTrailDiff::BatchBoundaryResolver

Inherits:
Object
  • Object
show all
Defined in:
lib/paper_trail_diff/batch_boundary_resolver.rb,
sig/generated/paper_trail_diff/batch_boundary_resolver.rbs

Overview

Resolves :first and :last endpoints for a whole batch in two queries per model class. Left to the caller this is a per-root lookup, which reintroduces exactly the queries a batched comparison exists to remove.

Constant Summary collapse

BOUNDARIES =

Returns:

  • (Object)
%i[first last].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pairs) ⇒ BatchBoundaryResolver

: (Array[[untyped, untyped]]) -> void

Parameters:

  • (Array[[ untyped, untyped ]])


19
20
21
# File 'lib/paper_trail_diff/batch_boundary_resolver.rb', line 19

def initialize(pairs)
  @pairs = pairs
end

Class Method Details

.symbolic?(endpoint) ⇒ Boolean

: (untyped) -> bool

Parameters:

  • (Object)

Returns:

  • (Boolean)


13
14
15
# File 'lib/paper_trail_diff/batch_boundary_resolver.rb', line 13

def symbolic?(endpoint)
  endpoint.is_a?(Symbol)
end

Instance Method Details

#add_model_boundaries(index, model_class, ids) ⇒ void

This method returns an undefined value.

: (Hash[Array[String], Hash[Symbol, untyped]], untyped, Array) -> void

Parameters:

  • (Hash[Array[String], Hash[Symbol, untyped]])
  • (Object)
  • (Array[untyped])


102
103
104
105
106
107
108
109
110
# File 'lib/paper_trail_diff/batch_boundary_resolver.rb', line 102

def add_model_boundaries(index, model_class, ids)
  first_ids, last_ids, versions = boundary_versions(model_class, ids)
  ids.each do |id|
    index[identity_key(model_class, id)] = {
      first: versions[boundary_key(first_ids, id)],
      last: versions[boundary_key(last_ids, id)]
    }
  end
end

#anchor(endpoint) ⇒ [ untyped, untyped ]?

: (untyped) -> [untyped, untyped]?

Parameters:

  • (Object)

Returns:

  • ([ untyped, untyped ], nil)


89
90
91
92
93
94
95
96
97
98
99
# File 'lib/paper_trail_diff/batch_boundary_resolver.rb', line 89

def anchor(endpoint)
  return if endpoint.nil? || symbolic?(endpoint)

  if Endpoint.version?(endpoint)
    [Endpoint.model_class(endpoint), endpoint.item_id]
  elsif Endpoint.record?(endpoint)
    [endpoint.class, endpoint.id]
  end
rescue InvalidEndpointError
  nil
end

#anchor_identity(endpoint) ⇒ Array[String]?

A symbol carries no identity of its own, so the pair's other endpoint has to say which record is meant. : (untyped) -> Array?

Parameters:

  • (Object)

Returns:

  • (Array[String], nil)


66
67
68
69
70
71
72
# File 'lib/paper_trail_diff/batch_boundary_resolver.rb', line 66

def anchor_identity(endpoint)
  return if endpoint.nil? || symbolic?(endpoint)

  Endpoint.identity(endpoint)
rescue InvalidEndpointError
  nil
end

#anchorsArray[[ untyped, untyped ]]

: () -> Array[[untyped, untyped]]

Returns:

  • (Array[[ untyped, untyped ]])


84
85
86
# File 'lib/paper_trail_diff/batch_boundary_resolver.rb', line 84

def anchors
  @pairs.flatten(1).filter_map { |endpoint| anchor(endpoint) }.uniq
end

#boundary_indexHash[Array[String], Hash[Symbol, untyped]]

: () -> Hash[Array[String], Hash[Symbol, untyped]]

Returns:

  • (Hash[Array[String], Hash[Symbol, untyped]])


75
76
77
78
79
80
81
# File 'lib/paper_trail_diff/batch_boundary_resolver.rb', line 75

def boundary_index
  index = {} #: Hash[Array[String], Hash[Symbol, untyped]]
  anchors.group_by { |model_class, _id| model_class }.each do |model_class, entries|
    add_model_boundaries(index, model_class, entries.map { |_klass, id| id }.uniq)
  end
  index
end

#boundary_key(grouped, id) ⇒ Object

Grouped keys come back with whatever type the column uses, so match on the string form rather than assuming integers. : (Hash[untyped, untyped], untyped) -> untyped

Parameters:

  • (Hash[untyped, untyped])
  • (Object)

Returns:

  • (Object)


127
128
129
130
# File 'lib/paper_trail_diff/batch_boundary_resolver.rb', line 127

def boundary_key(grouped, id)
  key = grouped.keys.find { |candidate| candidate.to_s == id.to_s }
  grouped[key]
end

#boundary_versions(model_class, ids) ⇒ [ Hash[untyped, untyped], Hash[untyped, untyped], Hash[untyped, untyped] ]

Two grouped queries name each root's outermost versions, and one more loads them, whatever the size of the batch. : (untyped, Array) -> [Hash[untyped, untyped], Hash[untyped, untyped], Hash[untyped, untyped]]

Parameters:

  • (Object)
  • (Array[untyped])

Returns:

  • ([ Hash[untyped, untyped], Hash[untyped, untyped], Hash[untyped, untyped] ])


115
116
117
118
119
120
121
122
# File 'lib/paper_trail_diff/batch_boundary_resolver.rb', line 115

def boundary_versions(model_class, ids)
  version_class = model_class.paper_trail.version_class
  scope = version_class.where(item_type: model_class.base_class.name.to_s, item_id: ids)
  first_ids = scope.group(:item_id).minimum(:id)
  last_ids = scope.group(:item_id).maximum(:id)
  loaded = version_class.where(id: (first_ids.values + last_ids.values).uniq).index_by(&:id)
  [first_ids, last_ids, loaded]
end

#callArray[[ untyped, untyped ]]

Returns the pairs with every resolvable symbol replaced. A symbol that names history the record does not have is left in place, so the caller can decide what an absent history means rather than being handed nil. : () -> Array[[untyped, untyped]]

Returns:

  • (Array[[ untyped, untyped ]])


27
28
29
30
31
32
33
34
# File 'lib/paper_trail_diff/batch_boundary_resolver.rb', line 27

def call
  return @pairs unless @pairs.flatten(1).any? { |endpoint| symbolic?(endpoint) }

  index = boundary_index
  @pairs.map do |from, to|
    [resolve(from, to, index), resolve(to, from, index)]
  end
end

#identity_key(model_class, id) ⇒ Array[String]

: (untyped, untyped) -> Array

Parameters:

  • (Object)
  • (Object)

Returns:

  • (Array[String])


133
134
135
# File 'lib/paper_trail_diff/batch_boundary_resolver.rb', line 133

def identity_key(model_class, id)
  [model_class.base_class.name.to_s, id.to_s]
end

#resolve(endpoint, other, index) ⇒ Object

: (untyped, untyped, Hash[Array[String], Hash[Symbol, untyped]]) -> untyped

Parameters:

  • (Object)
  • (Object)
  • (Hash[Array[String], Hash[Symbol, untyped]])

Returns:

  • (Object)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/paper_trail_diff/batch_boundary_resolver.rb', line 46

def resolve(endpoint, other, index)
  return endpoint unless symbolic?(endpoint)

  unless BOUNDARIES.include?(endpoint)
    raise ConfigurationError,
          "unsupported boundary: #{endpoint.inspect}; use :first, :last, a version, or a record"
  end

  identity = anchor_identity(other)
  unless identity
    raise ConfigurationError,
          'a :first or :last endpoint needs the other endpoint to name a record'
  end

  index.dig(identity, endpoint) || endpoint
end

#symbolic?(endpoint) ⇒ Boolean

: (untyped) -> bool

Parameters:

  • (Object)

Returns:

  • (Boolean)


41
42
43
# File 'lib/paper_trail_diff/batch_boundary_resolver.rb', line 41

def symbolic?(endpoint)
  self.class.symbolic?(endpoint)
end