Module: HasHelpers::ActiveRecord

Includes:
ActiveRecordUtils
Defined in:
lib/has_helpers/active_record.rb,
lib/has_helpers/active_record/base.rb,
lib/has_helpers/active_record/date_range.rb,
lib/has_helpers/active_record/validation.rb,
lib/has_helpers/active_record/nested_resource.rb

Defined Under Namespace

Modules: Base, DateRange, NestedResource, Validation Classes: ServiceError

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ActiveRecordUtils

#key_with_prefix, #metadata_colums, #remove_prefix

Class Method Details

.relation_exists?(relation_name) ⇒ Boolean

TODO remove this in the future when elastic search or web services are implemented

Returns:

  • (Boolean)


512
513
514
# File 'lib/has_helpers/active_record.rb', line 512

def self.relation_exists?(relation_name)
  ::ActiveRecord::Base.connection.execute("SELECT 1 FROM #{ relation_name } LIMIT 1") rescue false
end

Instance Method Details

#add_joins(params, query, organization_id) ⇒ Object

In case we have a nested association and an organization_id , iterate over and add the neccessary joins



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/has_helpers/active_record.rb', line 170

def add_joins(params, query, organization_id)
  params.each do |key, _value|
    joins = key.to_s.split(".")
    if joins.size > 1
      joins[0...-1].each do |join_table|
        association_klass = ::HasHelpers::ApplicationHelper.translate_klass_name(join_table.singularize.capitalize).safe_constantize
        table_name = self.reflect_on_association(join_table.singularize.to_sym)&.klass ? join_table.singularize.to_sym : join_table.to_sym # only singularize association if it's needed
        query = query.joins(table_name)
        query = query.where("#{join_table}.organization_id = (?)", organization_id) if organization_id.present? && association_klass.present? && association_klass.has_attribute?(:organization_id)
      end
    end
  end
  query = query.where(organization_id: organization_id) if organization_id.present? && params.any? && !organization_attribute_exist?
  query
end

#assign_data(data, prefix: nil, current_user: nil, multi: nil) ⇒ Object

This method assign the data considering associations and user context. returns a hash containing the hydrated model and any associations with errors.



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/has_helpers/active_record.rb', line 223

def assign_data(data, prefix: nil, current_user: nil, multi: nil)
  klass = self
  model = klass.new

  reflections = klass.reflections # has_many, belons_to, has_one
  date_columns = klass.columns.select { |column| column.type == :date }.map(&:name) # start_date, end_date
  columns = klass.column_names - reflections&.keys - date_columns # name, is_adjustment, note

  associations_with_errors = {}

  columns.each do |column|
    model[column] = data[key_with_prefix(column, prefix)] if data[key_with_prefix(column, prefix)].present?
  end

  date_columns.each do |column|
    model.assign_attributes({ column.to_sym => data[key_with_prefix(column, prefix)] }) if data[key_with_prefix(column, prefix)].present?
  end

  if current_user.present?
    model.organization = current_user&.organizations&.first
    model.created_by_id = current_user&.id if model.has_attribute?("created_by_id")
    model.updated_by_id = current_user&.id if model.has_attribute?("updated_by_id")
  end

  reflections.each do |association_name, reflection|
    result = nil
    if !.include?(association_name)
      association = reflection.options[:class_name].present? ? reflection.options[:class_name].safe_constantize : (reflection.klass || reflection.active_record)
      is_constant = association.include?(::HasHelpers::Constant)
      if is_constant
        if data[key_with_prefix("#{association_name}_id", prefix)].present?
          model.send("#{association_name}=", association.lookup(id: data[key_with_prefix("#{association_name}_id", prefix)]))
        else
          constant_value = data[key_with_prefix(association_name, prefix)] || data[key_with_prefix("#{association_name}_name", prefix)] # examples: policy_holder_type, policy_holder_type_id, policy_holder_type_name because the association name is only 'policy_holder_type'
          model.send("#{association_name}=", association.lookup(constant_value)) if constant_value.present?
        end
      else
        case reflection.macro
        when :belongs_to, :has_one
          association_data = remove_prefix(association, data, prefix.present? ? "#{prefix}_#{association_name}" : association_name)
          if association_data.any?
            result = association.lookup(association_data, model&.organization_id)
            result.nil? ? associations_with_errors[association_name] = association : model.send("#{association_name}=", result)
          end
        when :has_many
          if multi.present?
            (1..multi).each do |n|
              digit = n.humanize.underscore.despace
              association_name = association_name.singularize
              association_data = remove_prefix(association, data, prefix.present? ? "#{prefix}_#{association_name}_#{digit}" : "#{association_name}_#{digit}")
              if association_data.any?
                # TODO: improve for User model that no longer has organization_id
                result = model.has_attribute?(:organization_id) ? association.lookup(association_data, nil) : association.lookup(association_data, model&.organization_id)
                if result.present?
                  model.send("#{association_name.pluralize}") << result
                else
                  associations_with_errors[association] = association_name
                end
              end
            end

          end

        end
      end
    end
  end
  {
    model: model,
    associations_with_errors: associations_with_errors
  }
end

#dn_columnsObject



208
209
210
# File 'lib/has_helpers/active_record.rb', line 208

def dn_columns
  self.column_names.select { |item| item.end_with?("_dn") }
end

#get_column_type(column_or_attribute) ⇒ Object



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/has_helpers/active_record.rb', line 326

def get_column_type(column_or_attribute)
  model_name, column_name = column_or_attribute.to_s.split(".")

  if model_name.blank? || column_name.blank?
    return self.columns_hash[column_or_attribute]&.type
  end

  if model_name == self.table_name
    return self.columns_hash[column_name]&.type
  end

  reflection = self.reflections[model_name&.singularize] || self.reflections[model_name]
  class_name = reflection&.klass || model_name.classify.safe_constantize

  class_name&.columns_hash&.dig(column_name)&.type
end

#has_favorites(limit: nil, validate_proc: nil, limit_trigger: nil) ⇒ Object

Enable favorites

Examples

Create a favorite with the default limit trigger and validation:

has_favorites limit: 5

Create a favorite with a custom validation:

has_favorites validate_proc: ->(){ errors.add(:base, "Too many favorites") if limit > some_limit }

Create a favorite with a custom limit trigger: class LimitReportFavorites < ::HasHelpers::LimitFavorites def trigger_function <<-SQL CREATE OR REPLACE FUNCTION public."#function_name"() RETURNS TRIGGER LANGUAGE plpgsql AS $function$ DECLARE row_limit INTEGER; nrows INTEGER; resource_id TEXT; BEGIN row_limit := #limit;

            SELECT
              COUNT(*) INTO nrows
            FROM favorites
            INNER JOIN report.reports AS reports ON reports.id = favorites.report_id
            WHERE favorites.created_by_id = NEW.created_by_id
            AND reports.resource_id = (SELECT r.resource_id FROM report.reports AS r WHERE r.id = NEW.report_id);

            IF nrows > row_limit THEN
              SELECT
                format('%s-%s', 'Report', NEW.report_id::text)
              INTO resource_id;

              RAISE NOTICE 'favorite limit: resource_id = %', resource_id;
              RAISE 'Exceeded favorite limit of % for %', row_limit, resource_id;
            END IF;

            RETURN NULL;
          END
        $function$;
      SQL
    end
  end

has_favorites limit_trigger: ::LimitReportFavorites.new(limit: 5)


483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
# File 'lib/has_helpers/active_record.rb', line 483

def has_favorites(limit: nil, validate_proc: nil, limit_trigger: nil)
  class_name = self.name.demodulize
  association_name = class_name.underscore.to_sym
  foreign_key = class_name.foreign_key.to_sym

  has_many :favorites, class_name: "::HasHelpers::Favorite", dependent: :destroy
  ::HasHelpers::Favorite.belongs_to association_name, class_name: self.name, optional: true
  ::HasHelpers::Favorite.has_owner association_name if ::HasHelpers::Favorite.respond_to?(:has_owner)

  if limit
    ::HasHelpers::LimitFavorites.triggers << ::HasHelpers::LimitFavorites.new(klass: self, limit: limit)

    ::HasHelpers::Favorite.validate if: foreign_key do
      if ::HasHelpers::Favorite.where(created_by_id: created_by_id).where.not(foreign_key => nil).count >= limit
        errors.add(association_name, "Favorites are limited to #{limit} per #{association_name}.")
      end
    end
  end

  if validate_proc
    ::HasHelpers::Favorite.validate(if: foreign_key, &validate_proc)
  end

  if limit_trigger
    ::HasHelpers::LimitFavorites.triggers << limit_trigger
  end
end

#has_recently_viewed(*resources) ⇒ Object



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/has_helpers/active_record.rb', line 343

def has_recently_viewed(*resources)
  ::HasHelpers.recently_viewed_resources = resources.map { |r| r.to_s.demodulize.tableize.to_sym }

  class_eval do
    # Returns the recently viewed records for the given association.
    #
    # Examples
    #   class User < ActiveRecord::Base
    #     has_recently_viewed :advisor, :policy, "HasOutlook::DiscussionTemplateConfig"
    #   end
    #   user = ::HasHelpers::User.find(id)
    #
    #   user.recently_viewed(:advisor)
    #   user.recently_viewed(:discussion_template_config)
    def recently_viewed(association_name)
      select_clauses = ["recently_vieweds.*"]
      join_clauses = []

      resource_klass = ::HasHelpers::RecentlyViewed.reflect_on_association(association_name).klass
      resource_base_klass = resource_klass.base_class

      # Detect owner view
      unless resource_base_klass.instance_variable_defined?(:@owner_column_names)
        owner_relation_name = "owner_#{ resource_base_klass.table_name }_view"
        resource_base_klass.instance_variable_set(:@owner_relation_name, owner_relation_name)

        if resource_base_klass.respond_to?(:owner_klass) && resource_base_klass.owner_klass
          resource_base_klass.instance_variable_set(:@owner_column_names, resource_base_klass.owner_klass.column_names)
        elsif ::HasHelpers::ActiveRecord.relation_exists?(owner_relation_name) # Check if the owner view is present.
          # Detect column names for owner view.
          resource_base_klass.instance_variable_set(
            :@owner_column_names,
            Class.new(::ActiveRecord::Base) do
              self.table_name = owner_relation_name
            end.column_names
          )
        else
          # No owner view detected so join in the resource records
          resource_base_klass.instance_variable_set(:@owner_relation_name, resource_base_klass.table_name)
          resource_base_klass.instance_variable_set(
            :@owner_column_names,
            (resource_base_klass.column_names & %w[id name username number type])
          )
        end
      end

      # Add select clauses for owner columns.
      owner_relation_name = resource_base_klass.instance_variable_get(:@owner_relation_name)
      resource_base_klass.instance_variable_get(:@owner_column_names).each do |owner_column|
        select_clauses << "#{ owner_relation_name }.#{ owner_column } AS owner_#{ owner_column }"
      end

      join_clauses << "INNER JOIN #{ owner_relation_name } ON #{ owner_relation_name }.id = recently_vieweds.#{ resource_base_klass.name.foreign_key }"
      type_scope = ::HasHelpers::RecentlyViewed.where(owner_relation_name => { type: resource_klass }) if resource_klass != resource_base_klass

      scope = ::HasHelpers::RecentlyViewed.
        select(select_clauses.join(", ")).
        joins(join_clauses.join(" ")).
        where(created_by_id: id).
        where(organization_id: organization_id). # this included so that we only include records from the user's primary organization
      where("recently_vieweds.#{ resource_base_klass.name.foreign_key } IS NOT NULL").
        limit(::HasHelpers::HISTORY_MAX) # Needed because STI parent classes may have more records than the limit, up to number_of_descendant_classes * history_max.

      # type_scope may be nil or may scope to a sub-type, e.g. where(policies: { type: "Application" })
      if type_scope
        scope = scope.merge(type_scope)
      end

      scope
    end

    # this association is only used for the destroy callback
    has_many :all_recently_viewed, dependent: :destroy, foreign_key: "created_by_id", class_name: "::HasHelpers::RecentlyViewed"
  end

  resources.each do |r|
    base_resource_klass = r.to_s.classify.constantize

    # Add association to the given resource class and all of its descendants.
    [base_resource_klass, *base_resource_klass.descendants].each do |resource_klass|
      resource_klass.class_eval do
        has_many :all_recently_viewed, dependent: :destroy, class_name: "::HasHelpers::RecentlyViewed", foreign_key: r.to_s.singularize.foreign_key
      end
    end

    ::HasHelpers::RecentlyViewed.class_eval do
      belongs_to r.to_s.demodulize.singularize.underscore.to_sym, class_name: r.to_s.classify, optional: true

      base_resource_klass.descendants.each do |descendant_klass|
        belongs_to descendant_klass.name.demodulize.singularize.underscore.to_sym, foreign_key: r.to_s.singularize.foreign_key, optional: true
      end
    end
  end
end

#ignore_columnsObject

An array of columns that should be ignored or excluded, by default is an empty array



204
205
206
# File 'lib/has_helpers/active_record.rb', line 204

def ignore_columns
  []
end

#is_valid_association(association_values) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/has_helpers/active_record.rb', line 133

def is_valid_association(association_values)
  # the association needs to be singular, example for Clients : "demographic.ssn"
  # the association would be "demographic" and the column name "ssn"
  validation = self
  association_values_without_last = association_values[0...-1] # Create a new array without the last element
  column_name = association_values.last
  association_values_without_last.each do |association|
    validation = self.reflect_on_association(association.singularize.to_sym)&.klass
    validation ||= self.reflect_on_association(association.to_sym)&.klass
  end
  validation&.column_names&.include?(column_name)
end

#klass_available_columnsObject

returns the available columns for creating a new object of the model class.



213
214
215
# File 'lib/has_helpers/active_record.rb', line 213

def klass_available_columns
  self.column_names + klass_belongs_to_available_columns - ignore_columns - dn_columns - 
end

#klass_belongs_to_available_columnsObject



217
218
219
# File 'lib/has_helpers/active_record.rb', line 217

def klass_belongs_to_available_columns
  self.reflections.select { |_association_name, reflection| reflection.macro == :belongs_to }&.keys # client -> client_status, firm, organization
end

#lookup(params, organization_id, scope: nil, condition: LogicalOperator::AND, show_error: false, return_multiple: false, case_sensitive: true) ⇒ Class/nil/String

Lookup for models

Parameters:

  • params (Hash)

    The attribute:values that are used for the lookup

  • organization_id (Hash)

    The value that are used for improve the lookup and only search in a determined organization

  • scope (Hash) (defaults to: nil)

    Additional scope for the lookup, defining joins, where clauses, and logical conditions.

  • condition (String) (defaults to: LogicalOperator::AND)

    The condition with which the lookup is performed (AND/OR). Default value is and.

  • show_error (String) (defaults to: false)

    Boolean - Flag to indicate whether to show error messages or return nil when there are multiple or no results.

  • return_multiple (String) (defaults to: false)

    Boolean - Flag to indicate whether to return all the models found or not.

  • case_sensitive (Boolean) (defaults to: true)

    Boolean - Flag to indicate whether the lookup operation should be case-sensitive or case-insensitive.

Returns:

  • (Class/nil/String)

    Returns model instance or nil - The result of the lookup operation. If a single record is found, the instance is returned; otherwise, returns nil or an error message based on the show_error flag.



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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
91
92
# File 'lib/has_helpers/active_record.rb', line 29

def lookup(params, organization_id, scope: nil, condition: LogicalOperator::AND, show_error: false, return_multiple: false, case_sensitive: true)
  model = nil
  if params.is_a? Hash
    params.each do |key, _value|
      association_values = key.to_s.split(".")
      if !self.has_attribute?(key)
        params = params.except(key) unless association_values.size > 1 && is_valid_association(association_values)
      end
    end
    if params.any?
      if condition == LogicalOperator::OR
        model = add_joins(params, self, organization_id)
        model = query_condition(params, case_sensitive, model, LogicalOperator::OR)
      end

      if condition == LogicalOperator::AND
        model = add_joins(params, self, organization_id)
        model = query_condition(params, case_sensitive, model)
      end
    end
  else
    model = lookup_by_attributes_precedence(params, organization_id, case_sensitive, return_multiple)
  end
  # scope example: [
  # {:joins=>{:policies=>:advisors}, :where=>{:advisors=>{:npn=>11111529}}},
  # {:joins=>{:policy_holders=>[{:policy=>{:writing_advisors=>:contract}}]}, :where=>{:"contracts.individual_number"=>"V1BUZ9J44XGJHF", :"contracts.corporate_number"=>"V1BUZ9J44XGJHF"}, :condition=>"or"}]
  if scope.present?
    scope.each do |item|
      # join tables if specified in the scope item
      model = model.joins(item[:joins]) if item[:joins].present?
      if item[:condition].present? && item[:condition] == LogicalOperator::OR
        model = query_condition(item[:where], case_sensitive, model, LogicalOperator::OR)
      else
        model ||= self
        model = model.where(item[:where]) # LogicalOperator::AND by default
      end
    end
  end
  if return_multiple
    ::ActiveRecord::Base.transaction do
      return model.presence || []
    rescue => e
      ::ActiveRecord::Base.connection.rollback_db_transaction
      error_message = "SQL error in #{__method__}: #{e.message}"
      if show_error
        return error_message
      else
        Rails.logger.info error_message
      end
      []
    end
  else
    ::ActiveRecord::Base.transaction do
      model = model.limit(2) unless model.nil? # Limiting the query to retrieve a maximum of 2 records for improved performance and to prevent unnecessary processing.
      model.present? && model.size == 1 ? model.first : (show_error ? (model.present? && model.size > 1 ? "Multiple found." : "Not found.") : nil)
    rescue => e
      # In case of errors for example when we iterate over the lookup_attributes_precedence and try to match
      # an string with an uuid (id), avoid the error and continue looking for the next attribute
      ::Rails.logger.info "SQL error in #{__method__}: #{e.message}"
      ::ActiveRecord::Base.connection.rollback_db_transaction
      nil
    end
  end
end

#lookup_attributes_precedenceObject

This is a list of attributes of the model that will be used to search by. Each model should override this method to lookup by simple attribute. e.g. Advisor.lookup("123456", 4) where "123456" is the unique import_key Note that nested associations should be defined as: table_name.attribute e.g. demographics.ssn Example for Advisor -> lookup_attributes_precedence = { id: "id", import_key: "import_key", code_name: "demographics.code_name", ssn: "demographics.ssn"}



191
192
193
194
195
# File 'lib/has_helpers/active_record.rb', line 191

def lookup_attributes_precedence
  @lookup_attributes_precedence ||= {
    id: "id"
  }
end

#lookup_by_attributes_precedence(params, organization_id, case_sensitive, return_multiple) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/has_helpers/active_record.rb', line 94

def lookup_by_attributes_precedence(params, organization_id, case_sensitive, return_multiple)
  model = nil
  value = params
  self.lookup_attributes_precedence.each do |_key, lookup_attribute|
    model_found = false
    joins = lookup_attribute.split(".")
    query = self

    # We don’t add organization_id in the query if organization_id is present in lookup_attributes_precedence, because organization_id is no longer in users.
    query = query.where(organization_id: organization_id) if organization_id.present? && !organization_attribute_exist?

    if joins.size > 1
      joins[0...-1].each do |join_table|
        table_name = self.reflect_on_association(join_table.singularize.to_sym)&.klass ? join_table.singularize.to_sym : join_table.to_sym # only singularize association if it's needed
        query = query.joins(table_name) # the join need to be singularized and the where query uses the full string as 'demographics.ssn'
      end
    end

    if case_sensitive || lookup_attribute.to_s.include?("id") || [:text, :string].exclude?(get_column_type(lookup_attribute))
      model = query.where("#{lookup_attribute} = :param_value", param_value: value.to_s)
    else
      model = query.where("LOWER(#{lookup_attribute}) = :param_value", param_value: value.to_s.downcase)
    end
    ::ActiveRecord::Base.transaction do
      model = model.limit(2) if model && !return_multiple # Limiting the query to retrieve a maximum of 2 records for improved performance and to prevent unnecessary processing.
      model_found = true if model.length > 0
    rescue => e
      # In case of errors for example when we iterate over the lookup_attributes_precedence and try to match
      # an string with an uuid (id), avoid the error and continue looking for the next attribute
      ::Rails.logger.info "SQL error in #{__method__}: #{e.message}"
      ::ActiveRecord::Base.connection.rollback_db_transaction
      model = nil
      model_found = false
    end
    break if model_found
  end
  model
end

#organization_attribute_exist?Boolean

Checks whether the lookup_attributes_precedence hash contains the key :organization_id. Returns true if the key exists

Returns:

  • (Boolean)


199
200
201
# File 'lib/has_helpers/active_record.rb', line 199

def organization_attribute_exist?
  self.lookup_attributes_precedence.key?(:organization_id)
end

#query_condition(params, case_sensitive, model = nil, condition = LogicalOperator::AND) ⇒ Object

Custom method for constructing AND/OR conditions based on input parameters params example: :name=>"Gayla Barrows"



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/has_helpers/active_record.rb', line 298

def query_condition(params, case_sensitive, model = nil, condition = LogicalOperator::AND)
  table_name = self.table_name
  conditions = []
  query_params = {}

  params.each do |column, value|
    column_name_query = column.to_s.tr(".", "_") # convert column names to a standardized format for consistency example: organizations.id -> :organizations_id, demographic.ssn -> :demographic_ssn
    association_values = column.to_s.split(".")

    # If the column is part of an association (e.g., demographics.name),
    # use the column name as it is. If it's not an association (e.g., name, id, import_key),
    # set the table_column as "#{table_name}.#{column}" (e.g., users.name, users.id, users.import_key).
    table_column = association_values.size > 1 ? column : "#{table_name}.#{column}"

    if case_sensitive || column.to_s.include?("id") || [:text, :string].exclude?(get_column_type(table_column))
      conditions << "#{table_column} = :#{column_name_query}"
    else
      conditions << "LOWER(#{table_column}) = :#{column_name_query}"
    end

    # a hash of query parameters for use in the WHERE clause {:organizations_id=>1, :users_name=>"Naomi'Lynch"}
    query_params[column_name_query.to_sym] = case_sensitive ? value : value.to_s.downcase
  end

  query = conditions.join(" #{condition} ") # "organizations.id = :organizations_id OR users.name = :users_name"
  model.nil? ? where(query, query_params) : model.where(query, query_params)
end

#supported_columns_lookup(prefix = nil, multi = nil) ⇒ Object

Returns an array of valid columns for extraction search, such as advisor_id and advisor_import_key. The optional 'prefix' parameter appends a prefix to each column name (prefix + lookup attribute precedence), e.g., advisor_id, client_id, firm_id. When multi is set, the function generates columns with instance numbers, e.g., advisor_one_id, advisor_two_id, up to multi (advisor_multi_id).



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/has_helpers/active_record.rb', line 151

def supported_columns_lookup(prefix = nil, multi = nil)
  @prefix_supported_columns ||= {}
  @prefix_supported_columns ["#{prefix}, #{multi}"] ||=
    begin
      keys_without_prefix = self.lookup_attributes_precedence.keys
      return prefix.present? ? keys_without_prefix.map { |column| [prefix, column].join("_") } : keys_without_prefix.map(&:to_s) unless multi

      keys_supported_columns = []
      (1..multi).each do |n|
        digit = n.humanize.underscore.despace
        keys_without_prefix.each do |key|
          keys_supported_columns << [prefix, digit, key].compact.join("_")
        end
      end
      keys_supported_columns
    end
end