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.
emailNULL = a phone-only user;organization_idNULL = 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, oradd_reference. A required FK bundles two things:foreign_key: trueis pure referential integrity (never a business decision), whilenull: falseon the FK is a mandatory-ness decision that can flip (adocumentmay 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: falseis required, and enforced from the model side byDevDoc/Rails/EnumColumnNotNull. But an enum is a plainintegercolumn, 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
-- enumreason)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 EXCLUDESreferences/belongs_to(and the separateadd_referencemethod): a required foreign key SHOULD carrynull: 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 |