Class: RuboCop::Cop::DevDoc::Migration::BooleanColumnNotNull
- Inherits:
-
Base
- Object
- Base
- RuboCop::Cop::DevDoc::Migration::BooleanColumnNotNull
- Defined in:
- lib/rubocop/cop/dev_doc/migration/boolean_column_not_null.rb
Overview
Boolean columns must carry null: false.
Rationale
null: false is reserved for cases where NULL has no meaningful
interpretation — and a justified boolean is one of those cases.
NULL is outside the column's stated domain of false: it
is neither value, and every read site has to silently coerce it
(nil || false == false) or risk a footgun.
The line is drawn here for standardization and non-subjectivity.
Whether a regular column should be present is a business
decision open to debate (see DevDoc/Migration/AvoidNonNull),
but once the developer has justified a boolean via
DevDoc/Migration/AvoidBooleanColumn's escape hatch, the
boolean's non-null-ness is objective — it does not depend on
any further business decision — so it is enforced mechanically
rather than argued column-by-column.
This cop is the inverse of DevDoc/Migration/AvoidNonNull for
the boolean case: that cop flags null: false on regular
columns (including booleans, which it cannot distinguish from
other types); this cop REQUIRES null: false on booleans. The
contradiction is resolved at the disable site: when the
developer disables AvoidNonNull on a boolean with -- boolean
(alongside the AvoidBooleanColumn disable), this cop fires if
null: false is missing.
Adding null: false to a table with existing rows
Postgres refuses to add a NOT NULL column to a non-empty table
without a backfill. Use the model-validations backfill pattern
(sanctioned by DevDoc/Migration/AvoidColumnDefault):
def change
add_column :table, :flag, :boolean
reversible do |dir|
dir.up do
Table.reset_column_information
Table.where(flag: nil).find_each do |t|
t.flag = false
t.save!
end
end
end
# rubocop:disable DevDoc/Migration/AvoidNonNull -- boolean
change_column_null :table, :flag, false
# rubocop:enable DevDoc/Migration/AvoidNonNull
end
The application layer is the source of truth for the default
value on new records (e.g. via after_initialize or a factory).
default: in the migration is still disallowed by
DevDoc/Migration/AvoidColumnDefault.
Interaction with AvoidBooleanColumn
These two cops fire together on every boolean addition:
AvoidBooleanColumn: "reconsider using a boolean at all"BooleanColumnNotNull: "if you do, it must be NOT NULL"
Satisfy both by either changing the column type (timestamp /
enum / model method) or, for a justified boolean, disabling
AvoidBooleanColumn with -- boolean AND ensuring null: false
is present so this cop does not fire.
NOTE: This cop catches t.boolean, add_column ..., :boolean, and
change_column ..., :boolean (a type change to boolean) without
null: false. It does NOT scan db/schema.rb for legacy debt — it
only enforces the invariant on declarations it can see. Existing
nullable booleans are surfaced by inspection, not by this cop.
Constant Summary collapse
- MSG =
'Boolean columns must carry `null: false` — NULL is outside {true, false}. ' \ 'Backfill existing rows via the model, then `change_column_null`.'.freeze
Instance Method Summary collapse
Instance Method Details
#on_send(node) ⇒ Object
110 111 112 113 114 115 116 117 |
# File 'lib/rubocop/cop/dev_doc/migration/boolean_column_not_null.rb', line 110 def on_send(node) return unless boolean_column?(node) || add_column_boolean?(node) || change_column_to_boolean?(node) = node.arguments.find(&:hash_type?) return if && null_false_pair() add_offense(node) end |