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
- PostgreSQL 18 —
ALTER TYPE— a value can be added to an enum type; there is no statement to remove one
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.