Module: TimeTravel::ClassMethods

Defined in:
lib/time_travel_backup.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#enum_fieldsObject

Returns the value of attribute enum_fields.



31
32
33
# File 'lib/time_travel_backup.rb', line 31

def enum_fields
  @enum_fields
end

#enum_itemsObject

Returns the value of attribute enum_items.



31
32
33
# File 'lib/time_travel_backup.rb', line 31

def enum_items
  @enum_items
end

Instance Method Details

#as_of(effective_date, *identifiers) ⇒ Object



51
52
53
54
55
56
# File 'lib/time_travel_backup.rb', line 51

def as_of(effective_date, *identifiers)
  effective_record = history(*identifiers)
    .where("effective_from <= ?", effective_date)
    .where("effective_till > ?", effective_date)
  effective_record.first if effective_record.exists?
end

#base_update(record, attributes, raise_error: false) ⇒ Object



139
140
141
142
143
144
145
# File 'lib/time_travel_backup.rb', line 139

def base_update(record, attributes, raise_error: false)
  if ENV["TIME_TRAVEL_POSTGRES_MODE"]
    return base_update_sql(record, attributes, raise_error: raise_error)
  else
    return base_update_native(record, attributes, raise_error: raise_error)
  end
end

#base_update_native(record, attributes, raise_error: false) ⇒ Object



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/time_travel_backup.rb', line 147

def base_update_native(record, attributes, raise_error: false)
  begin
    return true if attributes.symbolize_keys!.empty?
    attributes = { effective_from: nil, effective_till: nil }.merge(attributes)
    raise(ActiveRecord::RecordInvalid.new(self)) unless record.validate_update(attributes)
  
    affected_records = fetch_history_for_correction
    affected_timeframes = get_affected_timeframes(affected_records)
  
    corrected_records = construct_corrected_records(affected_timeframes, affected_records, attributes)
    squished_records = squish_record_history(corrected_records)
  
    self.class.transaction do
      squished_records.each do |record|
        self.class.create!(
          record.merge(
            call_original: true,
            valid_from: current_time,
            valid_till: INFINITE_DATE)
        )
      end
  
      affected_records.each {|record| record.update_attribute(:valid_till, current_time)}
    end
    true
  rescue => e
    raise e if raise_error
    p "encountered error on update - #{e.message}"
    false
  end
end

#base_update_sql(record, update_attributes, raise_error: false) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/time_travel_backup.rb', line 179

def base_update_sql(record, update_attributes, raise_error: false)
  begin
    return true if update_attributes.symbolize_keys!.empty?
    update_attributes.except!(:call_original)
    attributes_for_validation = { effective_from: nil, effective_till: nil }.merge(update_attributes)
    raise(ActiveRecord::RecordInvalid.new(self)) unless record.validate_update(attributes_for_validation)
  
    update_attrs = update_attributes.merge(effective_from: effective_from, effective_till: effective_till, current_time: current_time).merge(timeline_clauses)
    self.class.update_history([update_attrs])
  rescue => e
    raise e if raise_error
    p "encountered error on update - #{e.message}"
    false
  end
end

#batch_sizeObject



104
105
106
# File 'lib/time_travel_backup.rb', line 104

def batch_size
  self.count
end

#db_timestamp(datetime) ⇒ Object



100
101
102
# File 'lib/time_travel_backup.rb', line 100

def db_timestamp(datetime)
  datetime.to_datetime.utc.strftime(PRECISE_TIME_FORMAT)
end

#enum_infoObject



108
109
110
111
112
# File 'lib/time_travel_backup.rb', line 108

def enum_info
  self.enum_items ||= defined_enums.symbolize_keys
  self.enum_fields ||= self.enum_items.keys
  [self.enum_fields, self.enum_items]
end

#has_history?(attributes) ⇒ Boolean

Returns:

  • (Boolean)


114
115
116
# File 'lib/time_travel_backup.rb', line 114

def has_history?(attributes)
  self.exists?(**timeline_clauses(attributes))
end

#history(*identifiers) ⇒ Object



46
47
48
49
# File 'lib/time_travel_backup.rb', line 46

def history(*identifiers)
  p identifiers
  where(valid_till: INFINITE_DATE, **timeline_clauses(identifiers)).order("effective_from ASC")
end

#history_absentObject



124
125
126
127
128
# File 'lib/time_travel_backup.rb', line 124

def history_absent
  if not self.has_history?
    self.errors.add(:base, "does not have history")
  end
end

#history_presentObject



118
119
120
121
122
# File 'lib/time_travel_backup.rb', line 118

def history_present
  if self.has_history?
    self.errors.add(:base, "already has history")
  end
end

#set_enum(attrs) ⇒ Object



92
93
94
95
96
97
98
# File 'lib/time_travel_backup.rb', line 92

def set_enum(attrs)
  enum_fields, enum_items = enum_info
  enum_fields.each do |key|
    string_value = attrs[key]
    attrs[key] = enum_items[key][string_value] unless string_value.blank?
  end
end

#terminate_timeline(attributes, effective_till, raise_error: false) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/time_travel_backup.rb', line 195

def terminate_timeline(attributes, effective_till, raise_error: false)
  begin
    current_time=Time.current
    effective_record = self.history(attributes).where(effective_till: INFINITE_DATE).first
    if effective_record.present?
      attributes = effective_record.attributes.except(*ignored_copy_attributes)
      self.transaction do
        self.create!(
          attributes.merge(
            call_original: true,
            effective_till: effective_till,
            valid_from: current_time,
            valid_till: INFINITE_DATE)
        )
        effective_record.update_attribute(:valid_till, current_time)
      end
    else
      raise "no effective record found"
    end
  rescue => e
    raise e if raise_error
    p "encountered error on delete - #{e.message}"
    false
  end
end

#time_travel_identifiersObject



32
33
34
# File 'lib/time_travel_backup.rb', line 32

def time_travel_identifiers
  raise "Please implement time_travel_identifier method to return an array of indentifiers to fetch a single timeline"
end

#timeline_clauses(*identifiers) ⇒ Object



37
38
39
40
41
42
43
44
# File 'lib/time_travel_backup.rb', line 37

def timeline_clauses(*identifiers)
  clauses = {}
  identifiers.flatten!
  time_travel_identifiers.each_with_index do | identifier_key, index |
    clauses[identifier_key] = identifiers[index]
  end
  clauses
end

#update_history(attribute_set, latest_transactions: false) ⇒ Object



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/time_travel_backup.rb', line 58

def update_history(attribute_set, latest_transactions: false)
  current_time = Time.current
  other_attrs = (self.column_names - ["id", "created_at", "updated_at", "valid_from", "valid_till"])
  empty_obj_attrs = other_attrs.map{|attr| {attr => nil}}.reduce(:merge!).with_indifferent_access
  query = ActiveRecord::Base.connection.quote(self.unscoped.where(valid_till: INFINITE_DATE).to_sql)
  table_name = ActiveRecord::Base.connection.quote(self.table_name)

  attribute_set.each_slice(batch_size).to_a.each do |batched_attribute_set|
    batched_attribute_set.each do |attrs|
      attrs.symbolize_keys!
      set_enum(attrs)
      attrs[:timeline_clauses], attrs[:update_attrs] = attrs.partition do  |key, value|
          key.in?(time_travel_identifiers.map(&:to_sym))
        end.map(&:to_h).map(&:symbolize_keys!)
      if attrs[:timeline_clauses].empty? || attrs[:timeline_clauses].values.any?(&:blank?)
        raise "Timeline identifiers can't be empty"
      end
      obj_current_time = attrs[:update_attrs].delete(:current_time) || current_time
      attrs[:effective_from] = db_timestamp(attrs[:update_attrs].delete(:effective_from) || obj_current_time)
      attrs[:effective_till] = db_timestamp(attrs[:update_attrs].delete(:effective_till) || INFINITE_DATE)
      attrs[:current_time] = db_timestamp(obj_current_time)
      attrs[:infinite_date] = db_timestamp(INFINITE_DATE)
      attrs[:empty_obj_attrs] = empty_obj_attrs.merge(attrs[:timeline_clauses])
    end
    attrs = ActiveRecord::Base.connection.quote(batched_attribute_set.to_json)
    begin
      result = ActiveRecord::Base.connection.execute("select update_bulk_history(#{query},#{table_name},#{attrs},#{latest_transactions})")
    rescue => e
      ActiveRecord::Base.connection.execute 'ROLLBACK'
      raise e
    end
  end
end

#update_timeline(attributes) ⇒ Object



130
131
132
133
134
135
136
137
# File 'lib/time_travel_backup.rb', line 130

def update_timeline(attributes)
  if self.has_history?(attributes)  
    base_update(record, attributes, raise_error: true)
    self.history.where(effective_from: effective_from).first
  else
    self.create(attributes)
  end
end