class RailsBestPractices::Reviews::AlwaysAddDbIndexReview

Review db/schema.rb file to make sure every reference key has a database index.

See the best practice details here rails-bestpractices.com/posts/2010/07/24/always-add-db-index/

Implementation:

Review process:

only check the command and command_calls nodes and at the end of review process,
if the receiver of command node is "create_table", then remember the table names
if the receiver of command_call node is "integer" or "string" or "bigint" and suffix with _id, then remember it as foreign key
if the receiver of command_call node is "string", the name of it is _type suffixed and there is an integer or string column _id suffixed, then remember it as polymorphic foreign key
if the receiver of command_call node is remembered as foreign key and it have argument non-false "index", then remember the index columns
if the receiver of command node is "add_index", then remember the index columns
after all of these, at the end of review process

    ActiveRecord::Schema.define(version: 20101201111111) do
      ......
    end

if there are any foreign keys not existed in index columns,
then the foreign keys should add db index.

Public Class Methods

new(options = {}) click to toggle source
Calls superclass method
# File lib/rails_best_practices/reviews/always_add_db_index_review.rb, line 31
def initialize(options = {})
  super(options)
  @index_columns = {}
  @foreign_keys = {}
  @table_nodes = {}
end

Private Instance Methods

combine_polymorphic_foreign_keys() click to toggle source

combine polymorphic foreign keys, e.g.

[tagger_id], [tagger_type] => [tagger_id, tagger_type]
# File lib/rails_best_practices/reviews/always_add_db_index_review.rb, line 170
def combine_polymorphic_foreign_keys
  @index_columns.each do |_table, foreign_keys|
    foreign_id_keys = foreign_keys.select { |key| key.size == 1 && key.first =~ /_id/ }
    foreign_type_keys = foreign_keys.select { |key| key.size == 1 && key.first =~ /_type/ }
    foreign_id_keys.each do |id_key|
      next unless type_key =
        foreign_type_keys.detect { |type_key| type_key.first == id_key.first.sub(/_id/, '') + '_type' }

      foreign_keys.delete(id_key)
      foreign_keys.delete(type_key)
      foreign_keys << id_key + type_key
    end
  end
end
greater_than_or_equal(more_array, less_array) click to toggle source

check if more_array is greater than less_array or equal to less_array.

# File lib/rails_best_practices/reviews/always_add_db_index_review.rb, line 192
def greater_than_or_equal(more_array, less_array)
  more_size = more_array.size
  less_size = less_array.size
  (more_array - less_array).size == more_size - less_size
end
not_indexed?(table, column) click to toggle source

check if the table’s column is indexed.

# File lib/rails_best_practices/reviews/always_add_db_index_review.rb, line 186
def not_indexed?(table, column)
  index_columns = @index_columns[table]
  !index_columns || index_columns.none? { |e| greater_than_or_equal(Array(e), Array(column)) }
end
remember_foreign_key_columns(node) click to toggle source

remember foreign key columns

# File lib/rails_best_practices/reviews/always_add_db_index_review.rb, line 118
def remember_foreign_key_columns(node)
  table_name = @table_name
  foreign_key_column = node.arguments.all.first.to_s
  @foreign_keys[table_name] ||= []
  if foreign_key_column =~ /(.*?)_id$/
    @foreign_keys[table_name] <<
      if @foreign_keys[table_name].delete("#{Regexp.last_match(1)}_type")
        ["#{Regexp.last_match(1)}_id", "#{Regexp.last_match(1)}_type"]
      else
        foreign_key_column
      end
    foreign_id_column = foreign_key_column
  elsif foreign_key_column =~ /(.*?)_type$/
    @foreign_keys[table_name] <<
      if @foreign_keys[table_name].delete("#{Regexp.last_match(1)}_id")
        ["#{Regexp.last_match(1)}_id", "#{Regexp.last_match(1)}_type"]
      else
        foreign_key_column
      end
    foreign_id_column = "#{Regexp.last_match(1)}_id"
  end

  if foreign_id_column
    index_node = node.arguments.all.last.hash_value('index')
    if index_node.present? && (index_node.to_s != 'false')
      @index_columns[table_name] ||= []
      @index_columns[table_name] << foreign_id_column
    end
  end
end
remember_index_columns_inside_table(node) click to toggle source

remember the node as index columns, when used inside a table block, i.e.

t.index [:column_name, ...]
# File lib/rails_best_practices/reviews/always_add_db_index_review.rb, line 103
def remember_index_columns_inside_table(node)
  table_name = @table_name
  index_column = node.arguments.all.first.to_object

  @index_columns[table_name] ||= []
  @index_columns[table_name] << index_column
end
remember_index_columns_outside_table(node) click to toggle source

remember the node as index columns, when used outside a table block, i.e.

add_index :table_name, :column_name
# File lib/rails_best_practices/reviews/always_add_db_index_review.rb, line 92
def remember_index_columns_outside_table(node)
  table_name = node.arguments.all.first.to_s
  index_column = node.arguments.all[1].to_object

  @index_columns[table_name] ||= []
  @index_columns[table_name] << index_column
end
remember_table_nodes(node) click to toggle source

remember table nodes

# File lib/rails_best_practices/reviews/always_add_db_index_review.rb, line 112
def remember_table_nodes(node)
  @table_name = node.arguments.all.first.to_s
  @table_nodes[@table_name] = node
end
remove_only_type_foreign_keys() click to toggle source

remove the non foreign keys with only _type column.

# File lib/rails_best_practices/reviews/always_add_db_index_review.rb, line 162
def remove_only_type_foreign_keys
  @foreign_keys.each do |_table, foreign_keys|
    foreign_keys.delete_if { |key| key.is_a?(String) && key =~ /_type$/ }
  end
end
remove_table_not_exist_foreign_keys() click to toggle source

remove the non foreign keys without corresponding tables.

# File lib/rails_best_practices/reviews/always_add_db_index_review.rb, line 150
def remove_table_not_exist_foreign_keys
  @foreign_keys.each do |table, foreign_keys|
    foreign_keys.delete_if do |key|
      if key.is_a?(String) && key =~ /_id$/
        class_name = Prepares.model_associations.get_association_class_name(table, key[0..-4])
        class_name ? !@table_nodes[class_name.gsub('::', '').tableize] : !@table_nodes[key[0..-4].pluralize]
      end
    end
  end
end