class RuboCop::Cop::Rails::UniqueValidationWithoutIndex

When you define a uniqueness validation in Active Record model, you also should add a unique index for the column. There are two reasons First, duplicated records may occur even if Active Record's validation is defined. Second, it will cause slow queries. The validation executes a `SELECT` statement with the target column when inserting/updating a record. If the column does not have an index and the table is large, the query will be heavy.

Note that the cop does nothing if db/schema.rb does not exist.

@example

# bad - if the schema does not have a unique index
validates :account, uniqueness: true

# good - if the schema has a unique index
validates :account, uniqueness: true

# good - even if the schema does not have a unique index
validates :account, length: { minimum: MIN_LENGTH }

Constants

MSG
RESTRICT_ON_SEND

Public Instance Methods

on_send(node) click to toggle source
# File lib/rubocop/cop/rails/unique_validation_without_index.rb, line 33
def on_send(node)
  return unless uniqueness_part(node)
  return if condition_part?(node)
  return unless schema

  klass, table, names = find_schema_information(node)
  return unless names
  return if with_index?(klass, table, names)

  add_offense(node)
end

Private Instance Methods

array_node_to_array(node) click to toggle source
# File lib/rubocop/cop/rails/unique_validation_without_index.rb, line 152
def array_node_to_array(node)
  node.values.map do |elm|
    case elm.type
    when :str, :sym
      elm.value
    else
      return nil
    end
  end
end
class_node(node) click to toggle source
# File lib/rubocop/cop/rails/unique_validation_without_index.rb, line 125
def class_node(node)
  node.each_ancestor.find(&:class_type?)
end
column_names(node) click to toggle source
# File lib/rubocop/cop/rails/unique_validation_without_index.rb, line 76
def column_names(node)
  arg = node.first_argument
  return unless arg.str_type? || arg.sym_type?

  ret = [arg.value]
  names_from_scope = column_names_from_scope(node)
  ret.concat(names_from_scope) if names_from_scope

  ret = ret.flat_map do |name|
    klass = class_node(node)
    resolve_relation_into_column(
      name: name.to_s,
      class_node: klass,
      table: schema.table_by(name: table_name(klass))
    )
  end
  ret.include?(nil) ? nil : ret.to_set
end
column_names_from_scope(node) click to toggle source
# File lib/rubocop/cop/rails/unique_validation_without_index.rb, line 95
def column_names_from_scope(node)
  uniq = uniqueness_part(node)
  return unless uniq.hash_type?

  scope = find_scope(uniq)
  return unless scope

  scope = unfreeze_scope(scope)

  case scope.type
  when :sym, :str
    [scope.value]
  when :array
    array_node_to_array(scope)
  end
end
condition_part?(node) click to toggle source
# File lib/rubocop/cop/rails/unique_validation_without_index.rb, line 140
def condition_part?(node)
  pairs = node.arguments.last
  return unless pairs.hash_type?

  pairs.each_pair.any? do |pair|
    key = pair.key
    next unless key.sym_type?

    key.value == :if || key.value == :unless
  end
end
find_schema_information(node) click to toggle source
# File lib/rubocop/cop/rails/unique_validation_without_index.rb, line 47
def find_schema_information(node)
  klass = class_node(node)
  return unless klass

  table = schema.table_by(name: table_name(klass))
  names = column_names(node)

  [klass, table, names]
end
find_scope(pairs) click to toggle source
# File lib/rubocop/cop/rails/unique_validation_without_index.rb, line 112
def find_scope(pairs)
  pairs.each_pair.find do |pair|
    key = pair.key
    next unless key.sym_type? && key.value == :scope

    break pair.value
  end
end
include_column_names_in_expression_index?(index, column_names) click to toggle source
# File lib/rubocop/cop/rails/unique_validation_without_index.rb, line 68
def include_column_names_in_expression_index?(index, column_names)
  return false unless (expression_index = index.expression)

  column_names.all? do |column_name|
    expression_index.include?(column_name)
  end
end
unfreeze_scope(scope) click to toggle source
# File lib/rubocop/cop/rails/unique_validation_without_index.rb, line 121
def unfreeze_scope(scope)
  scope.send_type? && scope.method?(:freeze) ? scope.children.first : scope
end
uniqueness_part(node) click to toggle source
# File lib/rubocop/cop/rails/unique_validation_without_index.rb, line 129
def uniqueness_part(node)
  pairs = node.arguments.last
  return unless pairs.hash_type?

  pairs.each_pair.find do |pair|
    next unless pair.key.sym_type? && pair.key.value == :uniqueness

    break pair.value
  end
end
with_index?(klass, table, names) click to toggle source
# File lib/rubocop/cop/rails/unique_validation_without_index.rb, line 57
def with_index?(klass, table, names)
  # Compatibility for Rails 4.2.
  add_indicies = schema.add_indicies_by(table_name: table_name(klass))

  (table.indices + add_indicies).any? do |index|
    index.unique &&
      (index.columns.to_set == names ||
       include_column_names_in_expression_index?(index, names))
  end
end