Alembic 1.18.4 の autogenerate は ALTER TABLE 系をかなり拾ってくれます。ただし公式ドキュメントの What does Autogenerate Detect (and what does it _not_ detect?) セクションには、検出できないものがはっきり並んでいます。テーブル名・カラム名の rename、CHECK 制約、PostgreSQL の ENUM 型、Sequence。autogenerate 任せにすると、これらは差分として出ないまま本番のマイグレーションに進みます。
autogenerate が見落とす変更の一覧
Alembic 1.18.4 のドキュメントには What does Autogenerate Detect (and what does it _not_ detect?) という長い見出しがあり、検出する/しないが項目でそのまま並んでいます。
| 変更内容 | autogenerate | 補い方 |
|---|---|---|
| テーブルの追加・削除 | 検出 | そのまま使える |
| カラムの追加・削除 | 検出 | そのまま使える |
| nullable の変更 | 検出 | そのまま使える |
| 名前付き unique / FK 制約の変更 | 検出 | そのまま使える |
| テーブル名の rename | 検出されない (add/drop で出る) | diff を破棄して op.rename_table() を手書き |
| カラム名の rename | 検出されない (add/drop で出る) | diff を破棄して op.alter_column(new_column_name=...) を手書き |
| CHECK 制約 / EXCLUDE / 匿名制約 | 検出されない | op.create_check_constraint() を手書き |
| PostgreSQL の ENUM 値追加・rename | 検出されない | op.execute("ALTER TYPE ...") を手書き |
| Sequence の追加・削除 | 検出されない | op.execute("CREATE SEQUENCE ...") を手書き |
ドキュメントの文末には autogenerate is not intended to be perfect とあり、生成後の手書きレビューが前提です。alembic revision --autogenerate -m "..." の結果を読まずにそのままコミットすると、rename 系はそのまま データを失う add/drop として実行されます。drop 側で旧カラムのデータが消失するので、autogenerate 後のレビューを省略するワークフローを組まないことが前提条件です。
ENUM 値の追加・rename を手書きで書く
PostgreSQL の ENUM は autogenerate が空ファイルを返す側の代表例で、Google サジェストにも alembic enum already exists、alembic autogenerate empty migration、alembic enum values to rename がそのまま上位に並びます。値を増やしたのにマイグレーションに何も入っていない、という状態の検索流入がそのまま反映されています。
autogenerate が空のマイグレーションを返す理由
SQLAlchemy 側で sqlalchemy.Enum の値を増やしても、autogenerate は ENUM 型の差分を出しません。公式の検出対象に Special SQLAlchemy types like Enum on non-supporting backends が明示的に 非対応 と書かれています。結果として alembic revision --autogenerate は upgrade / downgrade が pass だけの空ファイルを吐きます。
# models.py
from sqlalchemy import Enum
class OrderStatus(str, enum.Enum):
pending = "pending"
paid = "paid"
shipped = "shipped"
cancelled = "cancelled" # ← 新しく追加した値
$ alembic revision --autogenerate -m "add cancelled to order_status"
Generating /app/alembic/versions/8a1f3d_add_cancelled_to_order_status.py ... done
$ cat alembic/versions/8a1f3d_add_cancelled_to_order_status.py
def upgrade() -> None:
pass
def downgrade() -> None:
pass
「ENUM 値を増やしたのに upgrade が空」という状態は autogenerate のバグではなく、Alembic がそもそも検出しない仕様です。手書きで埋めます。
ALTER TYPE ADD VALUE はトランザクション内で実行できない
素直に op.execute("ALTER TYPE order_status ADD VALUE 'cancelled'") と書くと、PostgreSQL のバージョンによっては次のエラーが出ます。
psycopg.errors.ActiveSqlTransaction:
ALTER TYPE ... ADD cannot run inside a transaction block
Alembic はデフォルトで各マイグレーションをトランザクションで包みます。PostgreSQL 12 以降は ALTER TYPE ... ADD VALUE をトランザクション内で実行できる場合もありますが、IF NOT EXISTS や複数値の追加など条件が絡むと依然としてトランザクション外を要求してきます。安全側はマイグレーション単位でトランザクションを切る書き方です。
"""add cancelled to order_status
Revision ID: 8a1f3d
Revises: 7b2e0c
Create Date: 2026-05-07
"""
from alembic import op
revision = "8a1f3d"
down_revision = "7b2e0c"
branch_labels = None
depends_on = None
# このマイグレーションだけトランザクションで包まない
def upgrade() -> None:
with op.get_context().autocommit_block():
op.execute("ALTER TYPE order_status ADD VALUE IF NOT EXISTS 'cancelled'")
def downgrade() -> None:
# ENUM 値の削除は標準 SQL で書けない (後述)
raise NotImplementedError("PostgreSQL does not support DROP VALUE")
op.get_context().autocommit_block() はそのブロック内だけ自動コミットモードに切り替えるユーティリティです。これを使うと alembic enum already exists 系の二重実行も IF NOT EXISTS で防げます。
ENUM 値の rename と削除は別 ENUM 経由
PostgreSQL の ALTER TYPE には値の削除構文がありません。値の rename は PostgreSQL 10 以降 ALTER TYPE name RENAME VALUE 'old' TO 'new' が使えますが、こちらもトランザクション外推奨です。値を削除したい場合は新しい ENUM 型を作って差し替える定石になります。
def upgrade() -> None:
op.execute("CREATE TYPE order_status_new AS ENUM('pending','paid','shipped')")
op.execute("""
ALTER TABLE orders
ALTER COLUMN status TYPE order_status_new
USING status::text::order_status_new
""")
op.execute("DROP TYPE order_status")
op.execute("ALTER TYPE order_status_new RENAME TO order_status")
この4ステップで「ENUM 値の削除」が等価に表現できます。ただし ALTER COLUMN ... TYPE はテーブル全行を書き直すので、行数の多いテーブルでは SET lock_timeout = '5s' を前段に挟んで、長時間ロックを取る前にタイムアウトさせるほうが安全です。検証時に1000万行のテーブルでうっかり流して 30 分ブロックさせたことがあるので、本番投入前のリハーサルは省きません。
CHECK 制約・匿名制約・カラム rename の補い方
CHECK 制約・匿名制約・カラム rename も autogenerate は出力しません。手書きで補う方法はそれぞれ短く済みます。
CHECK 制約は op.create_check_constraint() で書く
SQLAlchemy 側のモデルに CheckConstraint を足しても、autogenerate は CHECK 制約を出力しません。手書きで op.create_check_constraint() を呼びます。
def upgrade() -> None:
op.create_check_constraint(
"ck_orders_total_positive",
"orders",
"total_amount >= 0",
)
def downgrade() -> None:
op.drop_constraint("ck_orders_total_positive", "orders", type_="check")
名前 (ck_orders_total_positive) を必ず付けるのがコツです。匿名で作ると後の drop で名前を引き当てられず、本番で constraint "ck_xxx" does not exist が出る要因になります。
匿名制約は naming_convention で名前を付けてから
SQLAlchemy 1.4 以降は MetaData(naming_convention=...) で UNIQUE / FK / CHECK 制約に自動で名前を付けられます。これを設定しておくと autogenerate が拾える対象が増えます。
from sqlalchemy import MetaData
NAMING_CONVENTION = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
metadata = MetaData(naming_convention=NAMING_CONVENTION)
既存の DB に後から導入する場合は、既存制約に既に付いている匿名名と衝突するので、alembic revision --autogenerate の差分を1度全部レビューしてから流します。
カラム rename は autogenerate の差分を捨てる
カラム名を変えると autogenerate は「古いカラムを drop」「新しいカラムを add」のペアを書きます。これを実行するとデータが消えます。生成された差分は破棄して、手書きの op.alter_column() に書き換えるのが正解です。
def upgrade() -> None:
op.alter_column(
"users",
"email", # 旧カラム名
new_column_name="email_address",
existing_type=sa.String(length=255),
existing_nullable=False,
)
existing_type と existing_nullable を省くと、ALTER 文によっては型情報が落ちて NOT NULL が外れることがあります。ここは省略せず明示しておくほうが事故を防ぎやすいです。SQLAlchemy 2.0の新API—select()と型ヒント対応で変わった書き方 で書いた型ヒント対応の延長として、マイグレーション側でも existing_type を書くと型情報を一貫させやすくなります。
branch / merge で並行開発を捌く
並行ブランチで別々のマイグレーションを作ると、Alembic は Multiple head revisions are present と警告して止まります。公式ドキュメントの Working with Branches と Merging Branches セクションに正規の解消手順が書かれています。
| 場面 | コマンド | 動き |
|---|---|---|
| 分岐を確認 | alembic branches --verbose | head が複数あるとここで2つ出る |
| マージ用 revision を作る | alembic merge -m "merge ae1 and 27c" ae1027 27c6a | 2つの head を統合する空マイグレーションを生成 |
| すべての head を一括 | alembic merge heads | 引数 heads で全 head をまとめて統合 |
| upgrade で head 指定 | alembic upgrade heads | マージしないまま並行で進めるときに使う |
Git の merge と違って Alembic の merge は 空のマイグレーションファイルを1本足すだけです。両ブランチの DDL は既にそれぞれの revision で実行済みなので、merge revision には upgrade(): pass しか入りません。マージ後は alembic heads が1つだけ返れば成功です。
まとめ
- Alembic 1.18.4 の autogenerate は テーブル/カラム rename・CHECK 制約・PostgreSQL ENUM・Sequence を検出しない。公式の
What does Autogenerate Detect (and what does it _not_ detect?)セクションが原典。 - ENUM 値の追加は
op.execute("ALTER TYPE ... ADD VALUE IF NOT EXISTS")をautocommit_block()で包む。値の削除は新 ENUM 経由で差し替える。 - CHECK 制約・匿名制約は
naming_conventionで名前を付けてから扱う。名前を付けないと downgrade で drop できない。 - カラム rename は autogenerate の add/drop ペアを捨てて
op.alter_column(new_column_name=...)を手書きする。existing_typeとexisting_nullableは省かない。 - 並行開発で head が分岐したら
alembic merge headsで空マイグレーションを足して統合。マージファイルに DDL は入らない。
autogenerate は alembic revision --autogenerate 1発で全部終わらせるためのものではなく、下書きを生成するためのアシストです。生成後にレビューして、検出されない領域を手書きで足すところまでがマイグレーション設計の本体になります。

