Skip to main content

PG.L4.ENUM_ADD_VALUE — Adding an enum value is a one-way change

  • Category: safety
  • Level: 4
  • Confidence: deterministic
  • Downtime class: online
  • Stability: stable
  • Suites: lint
  • Applies to: PostgreSQL 18

ALTER TYPE … ADD VALUE is cheap: it changes the catalog, not the table. What it is not is reversible. PostgreSQL offers no ALTER TYPE … DROP VALUE, so a down() written for this migration has nothing to call. A rollback undoes the rest of the deploy and leaves the new value in the type forever.

That is why this is a level-4 compatibility finding rather than a level-2 locking one. It costs no downtime and it cannot be taken back.

The workaround is worse than the alternative

Removing an enum value in practice means building a new type, rewriting the column with a USING clause, and dropping the old type — a table rewrite, under a lock, to undo a catalog change that took a millisecond. Nobody does this during a rollback.

Flagged

DB::statement("ALTER TYPE order_status ADD VALUE 'refunded'");

Preferred

// ALTER TYPE ... ADD VALUE has no reverse, so down() cannot undo it. Model the enum as a
// CHECK-constrained column instead, whose values a later migration can add or remove reversibly:
DB::statement("ALTER TABLE orders ADD CONSTRAINT orders_status_check CHECK (status IN ('open', 'closed', 'refunded'))");

A varchar with a CHECK gives the same guarantee at the same place — the database — and the set of allowed values becomes an ordinary, reversible, reviewable change. It is also what Laravel's own enum() column builder produces on PostgreSQL, so this is the idiomatic shape rather than a workaround.

If you keep the native type

Native enums have real advantages — a compact on-disk representation and a declared sort order — and choosing them is legitimate. Then say so, and accept that value additions are append-only:

#[SqlensIgnore('PG.L4.ENUM_ADD_VALUE', 'order_status is a native enum by design; values are append-only')]

Transaction restrictions

Older PostgreSQL versions refused ADD VALUE inside a transaction block entirely. Modern versions allow it but will not let the new value be used in the same transaction that added it — so a migration that adds a value and immediately writes a row with it fails. Splitting them across migrations avoids the whole question.

Sources

The fix material this rule carries

A finding from this rule carries machine-readable fix material, using this sequence:

  • enum_append_only — Extend the enumeration by appending, never by rewriting its existing members.

The payload is material for you or an agent to apply. SQLens writes no migration and runs no DDL. See the remediation payload for every field, the placeholder semantics, and the version rules a consumer has to follow.