Skip to main content

PG.L4.CHECK_ENUM_CHANGE — Changing a Laravel enum() column re-checks every row

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

A Laravel enum() column on PostgreSQL is not a native enum type. It is a varchar with a CHECK (col IN (…)) constraint. Changing the allowed values therefore compiles to two statements — drop the old constraint, add the new one — and the second of those validates every existing row while holding its lock.

There is a second problem underneath the first, and it is the one that causes the incident: between the drop and the add there is no window in which both the old and the new value set are accepted. During a rolling deploy, one of your two running releases is always writing a value the constraint rejects.

Flagged

DB::statement('ALTER TABLE orders DROP CONSTRAINT orders_status_check');
DB::statement("ALTER TABLE orders ADD CONSTRAINT orders_status_check CHECK (status IN ('open', 'closed', 'refunded'))");

Preferred

// Add the widened constraint NOT VALID and validate it, so writers can adopt the new
// value across a rolling deploy without a blocking re-check:
DB::statement("ALTER TABLE orders ADD CONSTRAINT orders_status_next CHECK (status IN ('open', 'closed', 'refunded')) NOT VALID");
DB::statement('ALTER TABLE orders VALIDATE CONSTRAINT orders_status_next');

The new constraint is additive: it exists alongside the old one until the old one is dropped in a later deploy. Both releases can write. The validation pass runs under SHARE UPDATE EXCLUSIVE, so nothing waits on it.

Widening and narrowing are not the same change

Adding an allowed value is compatible with every row already stored. Removing one is not — it can fail validation, and it must be preceded by migrating the rows that hold it. The rule flags the constraint swap either way, because from the migration alone the two are the same pair of statements; which one you have is in the value lists, and worth stating in your commit message.

  • PG.L2.CONSTRAINT_NOT_VALIDATED — the general form of the same NOT VALID argument.
  • PG.L4.ENUM_VALUE_REMOVED — the native-enum equivalent of the retirement problem.

Sources

The fix material this rule carries

A finding from this rule carries machine-readable fix material. Which sequence depends on the statement:

  • concurrently — Build the index without taking the write lock the ordinary form takes.
  • not_valid_then_validate — Add the constraint unvalidated, then validate it in a second migration.

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.