2025, Dec 10 03:02
Обновление ENUM в PostgreSQL через Alembic с autocommit
Как безопасно обновлять ENUM в PostgreSQL через Alembic: почему без коммита падают миграции, как применить autocommit_block для ALTER TYPE: стабильный деплой
Обновление перечислений (ENUM) в PostgreSQL через Alembic может оказаться коварно сложной задачей. Часто камнем преткновения становится ситуация, когда миграция данных добавляет новые элементы перечисления, а последующие файлы миграций падают, потому что эти значения не видны, пока не будет выполнен коммит. Попытка принудительно выполнить коммит на уровне CLI не помогает, а запуск Alembic дважды, чтобы «выжать» коммит между файлами, — неловкий костыль, о котором легко забыть в CI/CD.
Проблема
Симптом прост: новые значения ENUM должны быть зафиксированы, прежде чем их можно использовать. Без этого более поздний файл миграции, который опирается на только что добавленное значение, его просто не увидит. Быстрый, но грязный обходной путь — разбить обновление на два запуска, чтобы принудительно сделать коммит между шагами, например:
(alembic upgrade +1 && alembic upgrade head) || exit 0Официального флага, который заставлял бы Alembic делать коммит между файлами миграций, нет; опция вроде следующей не существует, как бы идеально она ни выглядела для этой ситуации:
alembic upgrade --commit headМиграция, добавляющая значение, может выглядеть так и всё равно упасть позже, потому что без коммита изменение не видно последующим файлам:
from alembic import op
def upgrade():
op.execute("ALTER TYPE rolename ADD VALUE IF NOT EXISTS 'OWNER'")
def downgrade():
passПочему это происходит
Как только новое значение добавлено в PostgreSQL ENUM, его нельзя использовать в последующих операциях, пока оно не будет зафиксировано. Если несколько файлов миграций выполняются без промежуточного коммита, всё, что зависит от нового значения перечисления, может падать из‑за проблем видимости. В результате корректное изменение будто «не вступает в силу», пока процесс не будет разделён или повторён — это хрупко и неожиданно.
Решение
Обёрните оператор ALTER TYPE в блок autocommit, чтобы изменение фиксировалось сразу. Тогда новый элемент перечисления станет виден остальной части последовательности обновления без необходимости запускать Alembic дважды.
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():
passЭтот приём точечный: он коммитит только изменение ALTER TYPE и оставляет остальной поток миграций как есть.
Вспомогательные утилиты для обновления ENUM
Если изменения ENUM повторяются, удобно централизовать генерацию и выполнение DDL. Следующие вспомогательные функции инкапсулируют тот же подход и выполняют все нужные операторы ALTER TYPE в одном autocommit‑блоке.
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)Почему это важно
Это избавляет от хрупких сценариев развёртывания и от необходимости разбивать обновления на несколько вызовов CLI только ради коммита. Миграции остаются детерминированными: перечисление обновляется контролируемо и явно, а последующие шаги сразу видят новые значения.
Выводы
Если миграции нужно добавить значения ENUM, которые должны быть доступны сразу, оборачивайте ALTER TYPE в autocommit‑блок. Не рассчитывайте на несуществующий флаг CLI для коммита между файлами миграций и не используйте двухшаговые хаки. Небольшой хелпер, инкапсулирующий DDL, делает повторяющиеся изменения перечислений чище и безопаснее.