Class: RuboCop::Cop::DevDoc::Migration::AvoidNonNull

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

Overview

Avoid null: false on regular columns.

Rationale

null: false on a regular column bakes a business rule (presence) into the schema. Presence belongs in the application layer (model validations), where it is easy to change.

The test for whether null: false is justified is "what would NULL mean for this column?":

  • If NULL is — or could become — a meaningful business state, presence is a business decision: keep it in the model. email NULL = a phone-only user; organization_id NULL = an unowned template.
  • If NULL is never a meaningful state by the nature of the data, it is a data-integrity concern and belongs in the schema (see Exception).

The line is drawn for standardization and non-subjectivity. Whether a regular column is "required" is subjective and invites per-column debate (email looks required until phone signup makes it optional), so the schema should not bake in that debatable call.

 Regular column
add_column :users, :profile_completion_rate, :float, null: false

✔️ Regular column
add_column :users, :profile_completion_rate, :float

Exception

null: false IS the right choice where NULL is never a meaningful state:

  • Required foreign keys — NOT flagged: this cop never looks at belongs_to, references, or add_reference. A required FK bundles two things: foreign_key: true is pure referential integrity (never a business decision), while null: false on the FK is a mandatory-ness decision that can flip (a document may later be an unowned template). Both are allowed in the schema pragmatically — the referential-integrity guarantee carries the mandatory-ness with it.

  • Enum columns — NULL is outside the enum's domain (a type violation), so null: false is required, and enforced from the model side by DevDoc/Rails/EnumColumnNotNull. But an enum is a plain integer column, statically indistinguishable from any other integer, so THIS cop cannot detect it and WILL flag it. Disable it on the line with a brief reason — -- enum — so the migration is self-documenting: a reader sees at a glance that the column is an enum.

    ✔️ Required foreign key (never flagged) t.belongs_to :user, null: false, foreign_key: true

    ✔️ Enum (flagged here — disable with a brief -- enum reason)

    rubocop:disable DevDoc/Migration/AvoidNonNull -- enum

    add_column :orders, :status, :integer, null: false

    rubocop:enable DevDoc/Migration/AvoidNonNull

NOTE: This cop is deliberately NOT enum-aware. It could read the model's enum declarations and skip those columns, but requiring an explicit per-line disable is intentional: it forces the developer to signal that the column is an enum, which documents the migration. A silent skip would hide that intent.

NOTE: This cop only flags null: false. It does not flag null: true (redundant but harmless), and it does not require foreign keys to carry null: false — adding it to an FK is encouraged but not enforced here.

Examples:

# bad
add_column :users, :name, :string, null: false

# bad (enum without a disable — the cop flags it; disable with `-- enum`)
t.integer :processing_status, null: false

# good
add_column :users, :name, :string

# good (required foreign key — never flagged)
t.belongs_to :user, null: false, foreign_key: true

Constant Summary collapse

MSG =
'Avoid `null: false` on regular columns; enforce presence in the model layer. ' \
'If this is an enum column, disable this cop on the line with a brief reason, e.g. `-- enum`.'.freeze
COLUMN_METHODS =

Column-definition helpers that take a null: option. Deliberately EXCLUDES references / belongs_to (and the separate add_reference method): a required foreign key SHOULD carry null: false, so those are never flagged.

%i[
  string integer float boolean datetime date text binary decimal
  json jsonb bigint
].freeze
RESTRICT_ON_SEND =
(COLUMN_METHODS + %i[add_column]).freeze

Instance Method Summary collapse

Instance Method Details

#on_send(node) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
# File 'lib/rubocop/cop/dev_doc/migration/avoid_non_null.rb', line 100

def on_send(node)
  return unless column_method?(node)

  options = node.arguments.find(&:hash_type?)
  return unless options

  pair = null_false_pair(options)
  return unless pair

  add_offense(pair)
end