class RuboCop::Cop::Rails::ReversibleMigration

This cop checks whether the change method of the migration file is reversible.

@example

# bad
def change
  change_table :users do |t|
    t.remove :name
  end
end

# good
def change
  create_table :users do |t|
    t.string :name
  end
end

# good
def change
  reversible do |dir|
    change_table :users do |t|
      dir.up do
        t.column :name, :string
      end

      dir.down do
        t.remove :name
      end
    end
  end
end

@example

# drop_table

# bad
def change
  drop_table :users
end

# good
def change
  drop_table :users do |t|
    t.string :name
  end
end

@example

# change_column_default

# bad
def change
  change_column_default(:suppliers, :qualification, 'new')
end

# good
def change
  change_column_default(:posts, :state, from: nil, to: "draft")
end

@example

# remove_column

# bad
def change
  remove_column(:suppliers, :qualification)
end

# good
def change
  remove_column(:suppliers, :qualification, :string)
end

@example

# remove_foreign_key

# bad
def change
  remove_foreign_key :accounts, column: :owner_id
end

# good
def change
  remove_foreign_key :accounts, :branches
end

# good
def change
  remove_foreign_key :accounts, to_table: :branches
end

@example

# change_table

# bad
def change
  change_table :users do |t|
    t.remove :name
    t.change_default :authorized, 1
    t.change :price, :string
  end
end

# good
def change
  change_table :users do |t|
    t.string :name
  end
end

# good
def change
  reversible do |dir|
    change_table :users do |t|
      dir.up do
        t.change :price, :string
      end

      dir.down do
        t.change :price, :integer
      end
    end
  end
end

@example

# remove_columns

# bad
def change
  remove_columns :users, :name, :email
end

# good
def change
  reversible do |dir|
    dir.up do
      remove_columns :users, :name, :email
    end

    dir.down do
      add_column :users, :name, :string
      add_column :users, :email, :string
    end
  end
end

# good (Rails >= 6.1, see https://github.com/rails/rails/pull/36589)
def change
  remove_columns :users, :name, :email, type: :string
end

@example

# remove_index

# bad
def change
  remove_index :users, name: :index_users_on_email
end

# good
def change
  remove_index :users, :email
end

# good
def change
  remove_index :users, column: :email
end

@see api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html

Constants

MSG

Public Instance Methods

on_block(node) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 222
def on_block(node)
  return unless within_change_method?(node)
  return if within_reversible_or_up_only_block?(node)
  return if node.body.nil?

  check_change_table_node(node.send_node, node.body)
end
on_send(node) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 209
def on_send(node)
  return unless within_change_method?(node)
  return if within_reversible_or_up_only_block?(node)

  check_irreversible_schema_statement_node(node)
  check_drop_table_node(node)
  check_reversible_hash_node(node)
  check_remove_column_node(node)
  check_remove_foreign_key_node(node)
  check_remove_columns_node(node)
  check_remove_index_node(node)
end

Private Instance Methods

all_hash_key?(args, *keys) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 352
def all_hash_key?(args, *keys)
  return false unless args&.hash_type?

  hash_keys = args.keys.map do |key|
    key.children.first.to_sym
  end

  (hash_keys & keys).sort == keys
end
check_change_table_node(node, block) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 279
def check_change_table_node(node, block)
  change_table_call(node) do |arg|
    if block.send_type?
      check_change_table_offense(arg, block)
    else
      block.each_child_node(:send) do |child_node|
        check_change_table_offense(arg, child_node)
      end
    end
  end
end
check_change_table_offense(receiver, node) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 315
def check_change_table_offense(receiver, node)
  method_name = node.method_name
  return if receiver != node.receiver &&
            reversible_change_table_call?(node)

  add_offense(
    node,
    message: format(MSG, action: "change_table(with #{method_name})")
  )
end
check_drop_table_node(node) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 238
def check_drop_table_node(node)
  drop_table_call(node) do
    unless node.parent.block_type? || node.last_argument.block_pass_type?
      add_offense(
        node,
        message: format(MSG, action: 'drop_table(without block)')
      )
    end
  end
end
check_irreversible_schema_statement_node(node) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 232
def check_irreversible_schema_statement_node(node)
  irreversible_schema_statement_call(node) do |method_name|
    add_offense(node, message: format(MSG, action: method_name))
  end
end
check_remove_column_node(node) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 260
def check_remove_column_node(node)
  remove_column_call(node) do |args|
    if args.to_a.size < 3
      add_offense(
        node,
        message: format(MSG, action: 'remove_column(without type)')
      )
    end
  end
end
check_remove_columns_node(node) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 291
def check_remove_columns_node(node)
  remove_columns_call(node) do |args|
    unless all_hash_key?(args, :type) && target_rails_version >= 6.1
      action = target_rails_version >= 6.1 ? 'remove_columns(without type)' : 'remove_columns'

      add_offense(
        node,
        message: format(MSG, action: action)
      )
    end
  end
end
check_remove_foreign_key_node(node) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 271
def check_remove_foreign_key_node(node)
  remove_foreign_key_call(node) do |arg|
    if arg.hash_type? && !all_hash_key?(arg, :to_table)
      add_offense(node, message: format(MSG, action: 'remove_foreign_key(without table)'))
    end
  end
end
check_remove_index_node(node) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 304
def check_remove_index_node(node)
  remove_index_call(node) do |args|
    if args.hash_type? && !all_hash_key?(args, :column)
      add_offense(
        node,
        message: format(MSG, action: 'remove_index(without column)')
      )
    end
  end
end
check_reversible_hash_node(node) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 249
def check_reversible_hash_node(node)
  return if reversible_change_table_call?(node)

  add_offense(
    node,
    message: format(
      MSG, action: "#{node.method_name}(without :from and :to)"
    )
  )
end
reversible_change_table_call?(node) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 326
def reversible_change_table_call?(node)
  case node.method_name
  when :change, :remove
    false
  when :change_default, :change_column_default, :change_table_comment,
       :change_column_comment
    all_hash_key?(node.arguments.last, :from, :to)
  else
    true
  end
end
within_change_method?(node) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 338
def within_change_method?(node)
  node.each_ancestor(:def).any? do |ancestor|
    ancestor.method?(:change)
  end
end
within_reversible_or_up_only_block?(node) click to toggle source
# File lib/rubocop/cop/rails/reversible_migration.rb, line 344
def within_reversible_or_up_only_block?(node)
  node.each_ancestor(:block).any? do |ancestor|
    (ancestor.block_type? &&
      ancestor.send_node.method?(:reversible)) ||
      ancestor.send_node.method?(:up_only)
  end
end