Class: ActiveRecord::Materialized::PartitionState

Inherits:
PartitionKeyedStore show all
Defined in:
lib/activerecord/materialized/partition_state.rb

Overview

Tracks which partitions of a cold view have been materialized ("fresh") so a read can decide whether a partition is served from the cache or read through to the source. Warm views are fully materialized and ignore this.

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from PartitionKeyedStore

#initialize

Constructor Details

This class inherits a constructor from ActiveRecord::Materialized::PartitionKeyedStore

Class Method Details

.cartesian(value_lists) ⇒ Object



88
89
90
91
92
93
94
# File 'lib/activerecord/materialized/partition_state.rb', line 88

def self.cartesian(value_lists)
  return nil if value_lists.any?(&:empty?)

  value_lists.reduce([[]]) do |tuples, values|
    tuples.flat_map { |tuple| values.map { |value| tuple + [value] } }
  end
end

.key_value_lists(conditions, group_keys) ⇒ Object



81
82
83
84
85
86
# File 'lib/activerecord/materialized/partition_state.rb', line 81

def self.key_value_lists(conditions, group_keys)
  normalized = conditions.transform_keys(&:to_s)
  return nil unless normalized.keys.sort == group_keys.sort

  group_keys.map { |column| Array(normalized.fetch(column)).map(&:to_s) }
end

.keys_from(view_class, args) ⇒ Object

The partition key tuples a query touches, or nil unless the conditions are an exact match on the GROUP BY columns (the only case the fast path serves).



63
64
65
66
67
68
69
70
71
72
# File 'lib/activerecord/materialized/partition_state.rb', line 63

def self.keys_from(view_class, args)
  conditions = single_hash(args)
  return nil if conditions.nil?

  group_keys = view_class.maintenance_key_columns
  return nil if group_keys.empty?

  value_lists = key_value_lists(conditions, group_keys)
  value_lists.nil? ? nil : cartesian(value_lists)
end

.single_hash(args) ⇒ Object



74
75
76
77
78
79
# File 'lib/activerecord/materialized/partition_state.rb', line 74

def self.single_hash(args)
  return nil unless args.length == 1

  conditions = args.fetch(0)
  conditions.is_a?(Hash) ? conditions : nil
end

Instance Method Details

#all_fresh?(key_tuples) ⇒ Boolean

Returns:

  • (Boolean)


9
10
11
12
13
14
15
16
17
18
19
20
# File 'lib/activerecord/materialized/partition_state.rb', line 9

def all_fresh?(key_tuples)
  return false if key_tuples.empty?

  ensure_table!
  view_class..ensure_schema! # the epoch subquery reads the metadata table
  serialized = key_tuples.map { |tuple| serialize(tuple) }.uniq
  # Only marks stamped with the current epoch count as fresh — a mark left behind by a populate
  # that raced a reset! (a widen invalidation) carries a superseded generation and is ignored.
  # The epoch is a correlated subquery so this stays ONE round-trip on the keyed cold-read fast path.
  fresh = scope.where(partition_key: serialized, generation: current_generation_scope)
  fresh.count == serialized.size
end

#current_generationObject

The current fresh-set epoch, read into Ruby. A populate captures this BEFORE its source read and stamps its marks with it, so a widen committing during the read advances the epoch and leaves the mark un-served. (all_fresh? compares against the epoch in-SQL via current_generation_scope.)



57
58
59
# File 'lib/activerecord/materialized/partition_state.rb', line 57

def current_generation
  view_class..fresh_set_generation
end

#mark_fresh!(key_tuples, generation:) ⇒ Object

Stamp each partition fresh with generation (the epoch the caller captured before its source read). Overwrites any existing mark's generation, so a re-populate after a reset! re-establishes freshness rather than leaving a stale-epoch row that all_fresh? would forever ignore.



25
26
27
28
29
30
31
32
33
# File 'lib/activerecord/materialized/partition_state.rb', line 25

def mark_fresh!(key_tuples, generation:)
  return if key_tuples.empty?

  ensure_table!
  key_tuples.uniq.each do |tuple|
    record = record_class.create_or_find_by(view_name: view_key, partition_key: serialize(tuple))
    record.update!(generation: generation) unless record.generation == generation
  end
end

#mark_stale!(key_tuples) ⇒ Object



35
36
37
38
39
40
# File 'lib/activerecord/materialized/partition_state.rb', line 35

def mark_stale!(key_tuples)
  return if key_tuples.empty?

  ensure_table!
  scope.where(partition_key: key_tuples.map { |tuple| serialize(tuple) }).delete_all
end

#reset!Object

Invalidate the whole fresh set (a widen recompute whose scope is unknown). Advance the epoch FIRST, then delete the marks: the epoch bump alone invalidates everything (all_fresh? honours only current-epoch marks), so the delete is pure cleanup. Ordering it this way makes reset! crash-safe without a transaction — a crash after the bump but before the delete still leaves the fresh set invalidated (stale-epoch marks are ignored, and a racing populate that captured the pre-bump epoch is ignored too); the leftover rows are reclaimed by the next reset!.



48
49
50
51
52
# File 'lib/activerecord/materialized/partition_state.rb', line 48

def reset!
  ensure_table!
  view_class..bump_fresh_set_generation!
  scope.delete_all
end