PG.L4.ENUM_VALUE_REMOVED — Renaming an enum value rewrites the meaning of stored data
- Category: safety
- Level: 4
- Confidence: heuristic
- Downtime class:
online - Stability: stable
- Suites: lint
- Applies to: PostgreSQL 18
The mirror image of the previous rule, and the more dangerous half.
ALTER TYPE … RENAME VALUE does not migrate data — it renames the label that existing rows already
point at. Every row stored as pending now reads as awaiting, and nothing recorded that they were
written under the old meaning. If the two names mean the same thing that is fine; if the rename is
part of a semantic change, the history has been silently rewritten.
Removing a value is worse still: PostgreSQL cannot do it at all without rebuilding the type, which
means a new type, an ALTER COLUMN … USING rewrite of the whole table, and a DROP TYPE.
Why the confidence is heuristic
The rule sees the statement, not your intent. A rename that fixes a typo and a rename that changes what a value means are the same SQL. It flags the shape and leaves the judgment where it belongs — which is also why the finding is straightforward to accept with a reason when the rename is cosmetic.
Flagged
DB::statement("ALTER TYPE order_status RENAME VALUE 'pending' TO 'awaiting'");
Preferred
// PostgreSQL cannot remove or rename an enum value in place without rebuilding the whole
// type; a CHECK-constrained text column lets a value be retired in one reversible statement:
DB::statement("ALTER TABLE orders ADD CONSTRAINT orders_status_check CHECK (status IN ('open', 'closed', 'awaiting'))");
Retiring a value safely
Whichever representation you use, the sequence is the same and it takes more than one deploy:
- Stop writing the old value; ship that.
- Migrate the rows that still hold it, in bounded batches.
- Only then remove it from the allowed set.
Step 3 first is how a CHECK violation takes down a deploy, and how an enum rebuild rewrites a
table for nothing.
Related
PG.L4.CHECK_ENUM_CHANGE covers the same change on a Laravel enum() column, which is a varchar
with a CHECK rather than a native type — a different statement with the same trap underneath.
Sources
- PostgreSQL 18 —
ALTER TYPE— a value can be renamed; removing one requires rebuilding the type
The fix material this rule carries
A finding from this rule carries machine-readable fix material, using this sequence:
expand_contract— Add the new shape, move readers and writers across, remove the old one — over three deploys.
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.