Schema migrations5 min read

Manage PostgreSQL enum changes across application releases

G

Guepard Team

November 14, 2026

PostgreSQL 18 scope. The SQL in this article was run on PostgreSQL 18.6 against synthetic fixtures.

An enum makes a set of labels part of the database schema. That gives constraints and readable values, but it also couples database changes to every application that decodes those labels into a closed set. Adding paused to a job-state enum may be easy for PostgreSQL and still crash an older worker that assumes only queued and done exist.

Treat enum evolution as a protocol change. There are three independent questions: can the database store the label, can every relevant reader understand it, and may writers begin producing it? The safe release order answers them separately.

Start with the actual PostgreSQL 18 rules

These examples require a disposable PostgreSQL 18 database and ownership of the enum type. PostgreSQL permits adding an enum value within a transaction, but that new value cannot be used until the transaction commits. A migration runner that bundles both schema and data changes into one transaction can therefore fail even though the DDL itself is valid.

CREATE TYPE job_state AS ENUM ('queued', 'done');
CREATE TABLE enum_job (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  state job_state NOT NULL DEFAULT 'queued'
);
INSERT INTO enum_job DEFAULT VALUES;

BEGIN;
ALTER TYPE job_state ADD VALUE 'paused' AFTER 'queued';
COMMIT;

-- A later transaction may now use the value.
UPDATE enum_job SET state = 'paused' WHERE id = 1;

The example intentionally uses two transactions. In a real release, the update should not happen merely because the first transaction committed. Reader compatibility is an additional gate. A database deployment can add the label days before any writer is allowed to emit it.

If migration tooling enforces one transaction per file, put the label addition in a dedicated migration and the first use in a later file or release. Verify the runner's behavior, including test-mode wrappers. A test framework that wraps the entire migration suite in a transaction can expose different behavior from the production runner.

Order is part of the contract

Enum comparison follows the enum's declared order, not alphabetical order. AFTER 'queued' makes paused sort between queued and done. That matters if queries use ranges, MIN, MAX, or ORDER BY state.

SELECT enumlabel, enumsortorder
FROM pg_enum
WHERE enumtypid = 'job_state'::regtype
ORDER BY enumsortorder;

SELECT id, state
FROM enum_job
WHERE state < 'done'::job_state
ORDER BY state, id;

The second query includes both queued and paused jobs under the declared ordering. Is that intended? If the application meant “jobs eligible to execute,” an explicit state predicate is clearer than an ordering comparison. A paused job may be less than done in enum order but not eligible for execution.

Avoid exposing enum ordinal positions as external numeric codes. The catalog's sort-order representation is an implementation detail, not a portable business identifier. Document external wire values independently.

Enum release compatibility gates

Reference release order: add the label, deploy tolerant readers, then enable writers. A committed schema change is not permission to emit new values.

Make readers tolerant before enabling writers

A reader might fail in several places: an ORM type decoder, an exhaustive language switch, a generated API client, or an analytics model that enumerates every state. Test more than the endpoint that writes the new value. Include queue consumers, audit exports, retry handlers, and administrative dashboards.

There are two common approaches. Deploy readers with explicit support for the new label before enabling writers, or deploy a deliberate unknown-state behavior that preserves the raw value and avoids unsafe actions. The latter should not turn an unfamiliar state into queued; that would convert a compatibility problem into unintended execution.

A feature flag can delay writer emission, but turning the flag off does not remove already stored values. After new values exist, rolling back application code requires either compatible readers or a validated data migration. Record that irreversibility boundary in the release plan.

Renaming and removal are different problems

PostgreSQL supports renaming an enum value, but clients that send the old literal will then fail. A rename is not an alias. Prefer an additive transition if both spellings must coexist during a rolling deployment, and define when old values are rewritten and old writers retired.

There is no simple DROP VALUE operation for an existing enum label. Removing labels generally requires designing a replacement type and migrating dependent columns after cleaning up the data. Defaults, views, functions, arrays, constraints, and casts can all depend on the original type. Do not edit system catalogs to simulate removal.

A lookup table can be a better model when values are frequently added, retired, translated, or decorated with metadata:

CREATE TABLE job_state_catalog (
  code text PRIMARY KEY,
  may_execute boolean NOT NULL,
  retired_at timestamptz
);
INSERT INTO job_state_catalog(code, may_execute)
VALUES ('queued', true), ('paused', false), ('done', false);

A foreign key to this table validates membership without making each new business state a type-level operation. It introduces joins and requires governance over catalog updates. It also does not automatically solve client decoding: an application can still hard-code a closed set of text values.

Release acceptance criteria

Review database transaction boundaries, old-client behavior, sort-dependent queries, and the first committed row containing the new label. Rehearse disabling the writer feature after that row exists. If the rollback plan assumes deleting all new-state rows, ask whether those rows carry real work or audit history that must survive.

Enums work best when the set is relatively stable and the ordering has a clear meaning. Their strength is a strong schema contract. Evolving that contract safely requires coordinating all consumers, not just issuing a successful ALTER TYPE.

Sources and review scope

G

Guepard Team

Guepard Engineering

Book a demo →