Class: Magick::Adapters::ActiveRecord

Inherits:
Base
  • Object
show all
Defined in:
lib/magick/adapters/active_record.rb

Instance Method Summary collapse

Constructor Details

#initialize(model_class: nil) ⇒ ActiveRecord

Returns a new instance of ActiveRecord.



6
7
8
9
10
11
12
13
14
15
16
17
18
# File 'lib/magick/adapters/active_record.rb', line 6

def initialize(model_class: nil)
  @model_class = model_class || default_model_class
  # Cache AR version check once at init time (hot path optimization)
  ar_major = ::ActiveRecord::VERSION::MAJOR
  ar_minor = ::ActiveRecord::VERSION::MINOR
  @use_json = ar_major >= 8 || (ar_major == 7 && ar_minor >= 1)
  # Verify table exists - raise clear error if it doesn't
  unless @model_class.table_exists?
    raise AdapterError, "Table 'magick_features' does not exist. Please run: rails generate magick:active_record && rails db:migrate"
  end
rescue StandardError => e
  raise AdapterError, "Failed to initialize ActiveRecord adapter: #{e.message}"
end

Instance Method Details

#all_featuresObject



81
82
83
84
85
# File 'lib/magick/adapters/active_record.rb', line 81

def all_features
  @model_class.pluck(:feature_name).uniq
rescue StandardError => e
  raise AdapterError, "Failed to get all features from ActiveRecord: #{e.message}"
end

#delete(feature_name) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/magick/adapters/active_record.rb', line 57

def delete(feature_name)
  feature_name_str = feature_name.to_s
  retries = 5
  begin
    @model_class.where(feature_name: feature_name_str).destroy_all
  rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::ConnectionTimeoutError => e
    # SQLite busy/locked errors - retry with exponential backoff
    if (e.message.include?('database is locked') || e.message.include?('busy') || e.message.include?('timeout')) && retries > 0
      retries -= 1
      sleep(0.01 * (6 - retries)) # Exponential backoff: 0.01, 0.02, 0.03, 0.04, 0.05
      retry
    end
    raise AdapterError, "Failed to delete from ActiveRecord: #{e.message}"
  rescue StandardError => e
    raise AdapterError, "Failed to delete from ActiveRecord: #{e.message}"
  end
end

#exists?(feature_name) ⇒ Boolean

Returns:

  • (Boolean)


75
76
77
78
79
# File 'lib/magick/adapters/active_record.rb', line 75

def exists?(feature_name)
  @model_class.exists?(feature_name: feature_name.to_s)
rescue StandardError => e
  raise AdapterError, "Failed to check existence in ActiveRecord: #{e.message}"
end

#get(feature_name, key) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/magick/adapters/active_record.rb', line 20

def get(feature_name, key)
  feature_name_str = feature_name.to_s
  record = @model_class.find_by(feature_name: feature_name_str)
  return nil unless record

  # Handle both Hash (from serialize) and Hash/JSON (from attribute :json)
  data = record.data || {}
  value = data.is_a?(Hash) ? data[key.to_s] : nil
  deserialize_value(value)
rescue StandardError => e
  raise AdapterError, "Failed to get from ActiveRecord: #{e.message}"
end

#get_all_data(feature_name) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/magick/adapters/active_record.rb', line 87

def get_all_data(feature_name)
  record = @model_class.find_by(feature_name: feature_name.to_s)
  return {} unless record

  data = record.data || {}
  return {} unless data.is_a?(Hash)

  data.each_with_object({}) do |(k, v), result|
    result[k.to_s] = deserialize_value(v)
  end
rescue StandardError => e
  raise AdapterError, "Failed to get all data from ActiveRecord: #{e.message}"
end

#load_all_features_dataObject



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/magick/adapters/active_record.rb', line 101

def load_all_features_data
  result = {}
  @model_class.find_each do |record|
    data = record.data || {}
    next unless data.is_a?(Hash)

    feature_data = {}
    data.each do |k, v|
      feature_data[k.to_s] = deserialize_value(v)
    end
    result[record.feature_name] = feature_data
  end
  result
rescue StandardError => e
  raise AdapterError, "Failed to load all features from ActiveRecord: #{e.message}"
end

#set(feature_name, key, value) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/magick/adapters/active_record.rb', line 33

def set(feature_name, key, value)
  feature_name_str = feature_name.to_s
  retries = 5
  begin
    @model_class.transaction do
      record = @model_class.lock.find_or_create_by!(feature_name: feature_name_str)
      data = record.data || {}
      data = {} unless data.is_a?(Hash)
      data[key.to_s] = serialize_value(value)
      record.update!(data: data, updated_at: defined?(Time.current) ? Time.current : Time.now)
    end
  rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::ConnectionTimeoutError => e
    # SQLite busy/locked errors - retry with linear backoff
    if (e.message.include?('database is locked') || e.message.include?('busy') || e.message.include?('timeout')) && retries > 0
      retries -= 1
      sleep(0.01 * (6 - retries))
      retry
    end
    raise AdapterError, "Failed to set in ActiveRecord: #{e.message}"
  rescue StandardError => e
    raise AdapterError, "Failed to set in ActiveRecord: #{e.message}"
  end
end

#set_all_data(feature_name, data_hash) ⇒ Object



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/magick/adapters/active_record.rb', line 118

def set_all_data(feature_name, data_hash)
  feature_name_str = feature_name.to_s
  retries = 5
  begin
    @model_class.transaction do
      record = @model_class.lock.find_or_create_by!(feature_name: feature_name_str)
      existing_data = record.data || {}
      existing_data = {} unless existing_data.is_a?(Hash)
      data_hash.each do |key, value|
        existing_data[key.to_s] = serialize_value(value)
      end
      record.update!(data: existing_data, updated_at: defined?(Time.current) ? Time.current : Time.now)
    end
  rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::ConnectionTimeoutError => e
    if (e.message.include?('database is locked') || e.message.include?('busy') || e.message.include?('timeout')) && retries > 0
      retries -= 1
      sleep(0.01 * (6 - retries))
      retry
    end
    raise AdapterError, "Failed to set all data in ActiveRecord: #{e.message}"
  rescue StandardError => e
    raise AdapterError, "Failed to set all data in ActiveRecord: #{e.message}"
  end
end