2025, Nov 27 03:00
PostgreSQL ENUM updates in Alembic: reliable migrations using ALTER TYPE in an autocommit block
Learn how to fix PostgreSQL ENUM migrations in Alembic by wrapping ALTER TYPE in an autocommit block, making new values visible immediately in CI/CD.
Updating PostgreSQL ENUMs through Alembic can be deceptively tricky. A common stumbling block appears when a data migration adds new enum members, and subsequent migration files fail because those values aren’t visible until a commit happens. Trying to force a commit at the CLI layer doesn’t help, and running Alembic twice to brute-force a commit between files is an awkward workaround that’s easy to forget in CI/CD.
Problem
The symptom is straightforward: new enum values must be committed before they can be used. Without that, a later migration file that relies on a freshly added value won’t see it. A quick-and-dirty workaround is to split the upgrade into two runs to force a commit between steps, for example:
(alembic upgrade +1 && alembic upgrade head) || exit 0There’s no official flag to make Alembic commit between migration files; a suggestion like the following doesn’t exist even though it looks ideal for the situation:
alembic upgrade --commit headA migration that adds a value might look like this and still fail later because the change isn’t visible to subsequent files without a commit:
from alembic import op
def upgrade():
op.execute("ALTER TYPE rolename ADD VALUE IF NOT EXISTS 'OWNER'")
def downgrade():
passWhy this happens
Once a new value is added to a PostgreSQL ENUM, it can’t be used by subsequent operations until it’s committed. If multiple migration files run without an intervening commit, anything that depends on the new enum value may fail with visibility issues. The result looks like a valid change “not taking effect” until the process is split or retried, which is both brittle and surprising.
Solution
Wrap the ALTER TYPE statement inside an autocommit block so the change is committed immediately. That makes the new enum member visible to the rest of the upgrade sequence without having to run Alembic twice.
from alembic import op
def upgrade():
tx = op.get_context()
with tx.autocommit_block():
op.execute("ALTER TYPE rolename ADD VALUE IF NOT EXISTS 'OWNER'")
def downgrade():
passThis pattern is narrowly targeted: it commits just the ALTER TYPE change and leaves the rest of the migration flow intact.
Utility helpers for ENUM updates
If enum changes are recurring, it’s handy to centralize the DDL generation and execution. The following helper functions encapsulate the same approach and execute all required ALTER TYPE statements inside a single autocommit block.
from enum import Enum
from alembic import op
def compose_enum_value_additions(enum_class: type[Enum], pg_enum_name: str, only_new: set[str]) -> list[str]:
"""Build ALTER TYPE statements for missing enum values."""
ddl_list: list[str] = []
for item in enum_class:
if not only_new or item.value in only_new:
ddl_list.append(
f"ALTER TYPE {pg_enum_name} ADD VALUE IF NOT EXISTS '{item.value.upper()}'"
)
return ddl_list
def apply_enum_changes(
enum_class: type[Enum], pg_enum_name: str, only_new: set[str] | None = None
) -> None:
"""Execute ALTER TYPE for values in enum_class that aren't in the DB yet."""
if only_new is None:
only_new = set()
ctx = op.get_context()
with ctx.autocommit_block():
for ddl in compose_enum_value_additions(enum_class, pg_enum_name, only_new):
op.execute(ddl)Why this matters
This avoids fragile deployment flows and removes the need to split upgrades into multiple CLI calls just to sneak in a commit. It also keeps migrations deterministic: the enum is updated in a controlled, explicit way, and later steps immediately see the new values.
Takeaways
If a migration needs to add ENUM values that must be usable right away, wrap the ALTER TYPE in an autocommit block. Don’t rely on a non-existent CLI flag to commit between migration files, and don’t bake in two-step upgrade hacks. Encapsulating the DDL in a small helper makes repeated enum changes cleaner and safer.