Class: PaperTrailDiff::BatchedRootVersions

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

Overview

Selects each root's versions for a batched range in a fixed number of queries. Only the range forms that mean the same thing for every root are supported: a shared wall-clock window, or each root's own whole history.

Instance Method Summary collapse

Constructor Details

#initialize(records, time_range:, version_scope: nil, live_endpoints: nil) ⇒ BatchedRootVersions

: (Array, time_range: TimeRange?, ?version_scope: untyped, ?live_endpoints: Hash[Array[String], untyped]?) -> void

Parameters:

  • (Array[untyped])
  • time_range: (TimeRange, nil)
  • version_scope: (Object) (defaults to: nil)
  • live_endpoints: (Hash[Array[String], untyped], nil) (defaults to: nil)


10
11
12
13
14
15
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 10

def initialize(records, time_range:, version_scope: nil, live_endpoints: nil)
  @records = records
  @time_range = time_range
  @version_scope = version_scope
  @live_endpoints = live_endpoints
end

Instance Method Details

#after_window(later, range) ⇒ Object

: (untyped, TimeRange) -> untyped

Parameters:

Returns:

  • (Object)


140
141
142
143
144
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 140

def after_window(later, range)
  return later[:created_at].gteq(range.end_time) if range.exclude_end?

  later[:created_at].gt(range.end_time)
end

#base_scope(model_class, ids) ⇒ Object

: (untyped, Array) -> untyped

Parameters:

  • (Object)
  • (Array[untyped])

Returns:

  • (Object)


147
148
149
150
151
152
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 147

def base_scope(model_class, ids)
  model_class.paper_trail.version_class.where(
    item_type: model_class.base_class.name.to_s,
    item_id: ids
  )
end

#callHash[Array[String], RootVersionPlan]

Returns a plan per record identity. : () -> Hash[Array[String], RootVersionPlan]

Returns:



19
20
21
22
23
24
25
26
27
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 19

def call
  return {} if @records.empty?

  selected = {} #: Hash[Array[String], RootVersionPlan]
  @records.group_by(&:class).each do |model_class, records|
    select_model(model_class, records, selected)
  end
  selected
end

#chosen_version_ids(model_class, ids, range) ⇒ Set[untyped]?

One extra query names the selected mutations without discarding the unfiltered versions the successor lookup still needs. : (untyped, Array, TimeRange?) -> Set?

Parameters:

Returns:

  • (Set[untyped], nil)


86
87
88
89
90
91
92
93
94
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 86

def chosen_version_ids(model_class, ids, range)
  scope = @version_scope
  return unless scope

  # A narrowed relation is the expected return. Active Support also gives
  # `pluck` to plain enumerables, so an array of versions works too.
  filtered = scope.call(range_scope(model_class, ids, range))
  Set.new(filtered.pluck(:id))
end

#grouped_versions(model_class, ids, range) ⇒ Hash[String, Array[untyped]]

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

Parameters:

Returns:

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


97
98
99
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 97

def grouped_versions(model_class, ids, range)
  ordered(range_scope(model_class, ids, range)).group_by { |version| version.item_id.to_s }
end

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

: (untyped, untyped) -> Array

Parameters:

  • (Object)
  • (Object)

Returns:

  • (Array[String])


160
161
162
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 160

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

#live_endpoint(key, record) ⇒ Object

The batch already loaded every root, so closing on current state reuses that rather than reading each record again. : (Array, untyped) -> untyped

Parameters:

  • (Array[String])
  • (Object)

Returns:

  • (Object)


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

def live_endpoint(key, record)
  loaded = @live_endpoints
  return unless loaded

  current = loaded.fetch(key, record)
  current unless current.destroyed?
end

#model_versions(model_class, ids) ⇒ [ Hash[String, Array[untyped]], Set[untyped]?, Hash[String, untyped] ]

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

Parameters:

  • (Object)
  • (Array[untyped])

Returns:

  • ([ Hash[String, Array[untyped]], Set[untyped]?, Hash[String, untyped] ])


50
51
52
53
54
55
56
57
58
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 50

def model_versions(model_class, ids)
  range = @time_range
  empty = {} #: Hash[String, untyped]
  [
    grouped_versions(model_class, ids, range),
    chosen_version_ids(model_class, ids, range),
    range ? trailing_versions(model_class, ids, range) : empty
  ]
end

#no_earlier_trailing(table, range) ⇒ Object

: (untyped, TimeRange) -> untyped

Parameters:

Returns:

  • (Object)


118
119
120
121
122
123
124
125
126
127
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 118

def no_earlier_trailing(table, range)
  arel = Object.const_get(:Arel) #: untyped
  later = table.alias('paper_trail_diff_later_roots')
  arel.const_get(:SelectManager).new
      .from(later)
      .project(arel.sql('1'))
      .where(preceding(table, later, range))
      .exists
      .not
end

#ordered(relation) ⇒ Array[untyped]

: (untyped) -> Array

Parameters:

  • (Object)

Returns:

  • (Array[untyped])


155
156
157
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 155

def ordered(relation)
  relation.reorder(created_at: :asc, id: :asc).to_a
end

#preceding(table, later, range) ⇒ Object

Mirrors the window's own end handling, so a version sitting exactly on an inclusive boundary stays inside the window instead of masking the trailing version that follows it. : (untyped, untyped, TimeRange) -> untyped

Parameters:

Returns:

  • (Object)


133
134
135
136
137
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 133

def preceding(table, later, range)
  same_item = later[:item_type].eq(table[:item_type])
                               .and(later[:item_id].eq(table[:item_id]))
  same_item.and(after_window(later, range)).and(later[:created_at].lt(table[:created_at]))
end

#range_scope(model_class, ids, range) ⇒ Object

: (untyped, Array, TimeRange?) -> untyped

Parameters:

Returns:

  • (Object)


102
103
104
105
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 102

def range_scope(model_class, ids, range)
  scope = base_scope(model_class, ids)
  range ? range.scope(scope) : scope
end

#select_model(model_class, records, selected) ⇒ void

This method returns an undefined value.

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

Parameters:



37
38
39
40
41
42
43
44
45
46
47
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 37

def select_model(model_class, records, selected)
  ids = records.map(&:id)
  in_range, chosen, trailing = model_versions(model_class, ids)
  records.each do |record|
    key = identity(model_class, record.id)
    selected[key] = versions_for(
      in_range.fetch(record.id.to_s, []), chosen, trailing[record.id.to_s],
      live_endpoint(key, record)
    )
  end
end

#trailing_versions(model_class, ids, range) ⇒ Hash[String, untyped]

One row per root: the earliest version after the window, found without a window function so the query stays portable. : (untyped, Array, TimeRange) -> Hash[String, untyped]

Parameters:

Returns:

  • (Hash[String, untyped])


110
111
112
113
114
115
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 110

def trailing_versions(model_class, ids, range)
  table = model_class.paper_trail.version_class.arel_table
  relation = range.trailing_scope(base_scope(model_class, ids))
  ordered(relation.where(no_earlier_trailing(table, range)))
    .to_h { |version| [version.item_id.to_s, version] }
end

#versions_for(in_range, chosen, after_range, live_endpoint) ⇒ RootVersionPlan

: (Array, Set?, untyped, untyped) -> RootVersionPlan

Parameters:

  • (Array[untyped])
  • (Set[untyped], nil)
  • (Object)
  • (Object)

Returns:



61
62
63
64
65
66
67
68
69
70
# File 'lib/paper_trail_diff/batched_root_versions.rb', line 61

def versions_for(in_range, chosen, after_range, live_endpoint)
  RootVersionSelection.new(
    in_range: in_range,
    selected: chosen ? in_range.select { |version| chosen.include?(version.id) } : in_range,
    after_range: after_range,
    windowed: !@time_range.nil?,
    filtered: !@version_scope.nil?,
    live_endpoint: live_endpoint
  ).call
end