Class: RuboCop::Cop::DevDoc::Migration::AvoidNonNull
- Inherits:
-
Base
- Object
- Base
- RuboCop::Cop::DevDoc::Migration::AvoidNonNull
- 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.
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) = node.arguments.find(&:hash_type?) return unless pair = null_false_pair() return unless pair add_offense(pair) end |