Class: RuboCop::Cop::DevDoc::Migration::AvoidColumnDefault

Inherits:
Base
  • Object
show all
Defined in:
lib/rubocop/cop/dev_doc/migration/avoid_column_default.rb

Overview

Avoid setting a default: value in migrations.

Rationale

Avoid adding business logic to the database. Keep it centralized in the application layer (controller or model) for easier maintenance and flexibility. A default: in a migration embeds a business-logic assumption into the database schema, which is harder to change later than code.

Instead of relying on a database default, set the value explicitly in the application — and for existing rows, backfill via a reversible migration that goes through model validations:


class AddProfileCompletionRateToUsers < ActiveRecord::Migration[6.1]
def change
  add_column :users, :profile_completion_rate, :float, default: 0.0
end
end


class AddProfileCompletionRateToUsers < ActiveRecord::Migration[6.1]
def change
  add_column :users, :profile_completion_rate, :float

  reversible do |dir|
    dir.up do
      # Make sure Rails picks up the new column.
      User.reset_column_information

      User.where(profile_completion_rate: nil).find_each do |user|
        user.profile_completion_rate = 0.0
        # This may fail if existing records are invalid (e.g. nil required fields).
        # In that case, fix those records first rather than bypassing validation.
        user.save!
      end
    end
  end
end
end

Forms covered

The cop catches default: set at column-creation time AND default: set later via change_column_default(..., to: <non-nil>). Both are the same anti-pattern — a permanent default living in the schema. change_column_default(..., to: nil) (removing a default) is not flagged; that is the cleanup form.

Exception (auto-detected)

For performance reasons (large tables with millions of records) or when using null: false, you may temporarily set a default and then immediately remove it in the same migration. The cop suppresses the offense when it detects a matching change_column_default ..., to: nil for the same table and column anywhere in the same method body (def change / def up), including inside reversible do |dir| dir.up.

The exception applies symmetrically:

  • add_column ... default: X paired with change_column_default ... to: nil → no offense

  • change_column_default ... to: X paired with change_column_default ... to: nil → no offense

    ✔ (no offense — two-step pattern auto-detected) add_column :users, :profile_completion_rate, :float, default: 0.0 change_column_default :users, :profile_completion_rate, from: 0.0, to: nil

Examples:

# bad
add_column :users, :score, :integer, default: 0

# bad
t.string :status, default: 'active'

# bad — permanent default set via change_column_default
change_column_default :users, :score, from: nil, to: 0

# good
add_column :users, :score, :integer

# good (temporary default immediately removed — two-step pattern)
add_column :users, :score, :integer, default: 0
change_column_default :users, :score, from: 0, to: nil

# good (removing an existing default)
change_column_default :users, :score, from: 0, to: nil

Constant Summary collapse

MSG =
'Avoid setting `default:` in migrations. Keep business logic defaults in the application layer.'.freeze
MSG_CHANGE =
'Avoid setting a non-nil default via `change_column_default`. ' \
'Keep business logic defaults in the application layer.'.freeze
COLUMN_METHODS =
%i[
  string integer float boolean datetime date text binary decimal
  json jsonb bigint primary_key references belongs_to
].freeze

Instance Method Summary collapse

Instance Method Details

#on_send(node) ⇒ Object



111
112
113
114
115
116
117
# File 'lib/rubocop/cop/dev_doc/migration/avoid_column_default.rb', line 111

def on_send(node)
  if node.method?(:change_column_default)
    check_change_column_default(node)
  elsif column_method?(node)
    check_column_method(node)
  end
end