If the column was already NOT NULL (e.g. you're only changing max_length or help_text), this is safe — add `# zdm: ignore R015` to suppress.

If this is a genuine nullable → NOT NULL transition, use the four-step pattern. Pick a stable constraint name like `<table>_<col>_not_null` so step 4 can DROP it precisely (PostgreSQL ALTER TABLE ADD CONSTRAINT has no IF NOT EXISTS, so partially-applied migrations need to be cleaned up before re-running):
  1. ALTER TABLE ... ADD CONSTRAINT <table>_<col>_not_null CHECK (col IS NOT NULL) NOT VALID;
  2. ALTER TABLE ... VALIDATE CONSTRAINT <table>_<col>_not_null;  -- table-scan without blocking writes
  3. ALTER TABLE ... ALTER COLUMN col SET NOT NULL;  -- PostgreSQL 12+: catalog-only, no scan, because the validated CHECK proves no NULLs. On 11 and earlier, SET NOT NULL still scans the table, weakening (but not eliminating) the benefit: VALIDATE in step 2 still runs under SHARE UPDATE EXCLUSIVE, allowing concurrent reads and writes; only the final SET NOT NULL takes the blocking lock.
  4. ALTER TABLE ... DROP CONSTRAINT <table>_<col>_not_null;  -- recommended cleanup; the CHECK is subsumed by NOT NULL, but keeping both forces every INSERT to re-evaluate the predicate
