jpskill.com
🛠️ 開発・MCP コミュニティ

py-alembic-patterns

Alembic migration patterns for PostgreSQL. Use when creating migrations, reviewing autogenerated migrations, or handling schema changes safely.

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o py-alembic-patterns.zip https://jpskill.com/download/17842.zip && unzip -o py-alembic-patterns.zip && rm py-alembic-patterns.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/17842.zip -OutFile "$d\py-alembic-patterns.zip"; Expand-Archive "$d\py-alembic-patterns.zip" -DestinationPath $d -Force; ri "$d\py-alembic-patterns.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して py-alembic-patterns.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → py-alembic-patterns フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-18
取得日時
2026-05-18
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

Alembic マイグレーションのパターン

問題提起

Alembic の自動生成は便利ですが、見落としがあったり、危険なマイグレーションを生成したりすることがあります。スキーマの変更はリスクが高く、不適切なマイグレーションはデータ損失やダウンタイムを引き起こします。すべてのマイグレーションは、人間によるレビューが必要です。


パターン: マイグレーションコマンド

# モデルの変更からマイグレーションを生成
uv run alembic revision --autogenerate -m "Add user preferences table"

# マイグレーションを適用
uv run alembic upgrade head

# 1つ前のマイグレーションにロールバック
uv run alembic downgrade -1

# 特定のリビジョンにロールバック
uv run alembic downgrade abc123

# 現在のリビジョンを表示
uv run alembic current

# マイグレーション履歴を表示
uv run alembic history

# 未適用のマイグレーションを表示
uv run alembic history --indicate-current

パターン: 自動生成されたマイグレーションのレビュー

自動生成されたマイグレーションは必ずレビューしてください。修正が必要なことがよくあります。

自動生成で検出されるもの

  • テーブルの作成/削除
  • カラムの追加/削除
  • カラムの型変更
  • 外部キーの変更
  • インデックスの変更 (場合による)

自動生成で見落とされるもの

  • カラム名の変更 (削除 + 追加と認識され、データ損失につながる)
  • テーブル名の変更 (同様の問題)
  • データマイグレーション
  • 制約名
  • 部分インデックス
  • 複雑なインデックスの変更
  • チェック制約
  • トリガーと関数
# ❌ 危険: カラム名変更のために自動生成されたもの
def upgrade():
    op.drop_column("users", "name")      # データ損失!
    op.add_column("users", sa.Column("full_name", sa.String()))

# ✅ 正しい: 手動での名前変更
def upgrade():
    op.alter_column("users", "name", new_column_name="full_name")

def downgrade():
    op.alter_column("users", "full_name", new_column_name="name")

パターン: 安全なマイグレーション構造

"""Add user preferences table.

Revision ID: abc123
Revises: def456
Create Date: 2024-01-15 10:30:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers
revision = "abc123"
down_revision = "def456"
branch_labels = None
depends_on = None


def upgrade() -> None:
    # 常に明示的に、デフォルトに頼らない
    op.create_table(
        "user_preferences",
        sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
        sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
        sa.Column("theme", sa.String(50), nullable=False, server_default="light"),
        sa.Column("notifications_enabled", sa.Boolean(), nullable=False, server_default="true"),
        sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
    )

    # 明示的なインデックス名
    op.create_index(
        "ix_user_preferences_user_id",
        "user_preferences",
        ["user_id"],
    )

    # 明示的な名前を持つ外部キー
    op.create_foreign_key(
        "fk_user_preferences_user_id",
        "user_preferences",
        "users",
        ["user_id"],
        ["id"],
        ondelete="CASCADE",
    )


def downgrade() -> None:
    # 常に downgrade を実装する!
    op.drop_constraint("fk_user_preferences_user_id", "user_preferences", type_="foreignkey")
    op.drop_index("ix_user_preferences_user_id", "user_preferences")
    op.drop_table("user_preferences")

パターン: Non-Nullable なカラムの追加

問題: 既存のテーブルに NOT NULL カラムを追加すると、テーブルにデータがある場合に失敗します。

# ❌ 間違い: テーブルにデータがある場合に失敗する
def upgrade():
    op.add_column("users", sa.Column("role", sa.String(50), nullable=False))

# ✅ 正しい: 3段階のプロセス
def upgrade():
    # ステップ 1: nullable として追加
    op.add_column("users", sa.Column("role", sa.String(50), nullable=True))

    # ステップ 2: 既存の行をバックフィル
    op.execute("UPDATE users SET role = 'member' WHERE role IS NULL")

    # ステップ 3: NOT NULL 制約を追加
    op.alter_column("users", "role", nullable=False)

def downgrade():
    op.drop_column("users", "role")

パターン: データマイグレーション

問題: スキーマ変更中に既存のデータを変換する必要がある。

from sqlalchemy import text

def upgrade():
    # データ操作のためのコネクションを取得
    connection = op.get_bind()

    # 新しいカラムを追加
    op.add_column("assessments", sa.Column("status", sa.String(20)))

    # データを移行
    connection.execute(
        text("""
            UPDATE assessments 
            SET status = CASE 
                WHEN completed_at IS NOT NULL THEN 'completed'
                WHEN started_at IS NOT NULL THEN 'in_progress'
                ELSE 'pending'
            END
        """)
    )

    # これで NOT NULL を安全に追加できる
    op.alter_column("assessments", "status", nullable=False)


def downgrade():
    op.drop_column("assessments", "status")

パターン: 大規模テーブルのマイグレーション

問題: 大規模テーブルのマイグレーションは、テーブルを長時間ロックする可能性があります。

def upgrade():
    # ✅ 正しい: インデックスを同時に追加 (ロックなし)
    op.execute(
        "CREATE INDEX CONCURRENTLY ix_events_user_id ON events (user_id)"
    )

    # 注: CONCURRENTLY は autocommit モードが必要です
    # マイグレーションファイルに追加:
    # from alembic import context
    # context.configure(transaction_per_migration=False)

def downgrade():
    op.execute("DROP INDEX CONCURRENTLY IF EXISTS ix_events_user_id")


# 大規模テーブルのカラム変更については、以下を検討してください。
# 1. 新しいカラムを追加 (nullable)
# 2. 別のスクリプトでバッチ処理でバックフィル
# 3. 別のマイグレーションで制約を追加

パターン: Enum の変更

問題: PostgreSQL の enum は変更が難しい。


# 既存の enum に値を追加する
def upgrade():
    # PostgreSQL 固有: enum に値を追加
    op.execute("ALTER TYPE assessment_status ADD VALUE 'archived'")

def downgrade():
    # PostgreSQL では enum の値を削除できません!
    # 選択肢:
    # 1. そのままにする (通常は問題ない)
    # 2. enum を再作成する (複雑、データマイグレーションが必要)
    pass


# 新しい enum を作成する
def upgrade():
    # 最初に enum 型を作成
    assessment_status = postgresql.ENUM(
        "draft", "active", "completed",

(原文がここで切り詰められています)
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

Alembic Migration Patterns

Problem Statement

Alembic autogenerate is convenient but misses things and sometimes generates dangerous migrations. Schema changes are high-risk - bad migrations cause data loss or downtime. Every migration needs human review.


Pattern: Migration Commands

# Generate migration from model changes
uv run alembic revision --autogenerate -m "Add user preferences table"

# Apply migrations
uv run alembic upgrade head

# Rollback one migration
uv run alembic downgrade -1

# Rollback to specific revision
uv run alembic downgrade abc123

# Show current revision
uv run alembic current

# Show migration history
uv run alembic history

# Show pending migrations
uv run alembic history --indicate-current

Pattern: Reviewing Autogenerated Migrations

ALWAYS review autogenerated migrations. They often need fixes.

What Autogenerate Catches

  • Table creation/deletion
  • Column addition/removal
  • Column type changes
  • Foreign key changes
  • Index changes (sometimes)

What Autogenerate Misses

  • Column renames (sees as drop + add = DATA LOSS)
  • Table renames (same problem)
  • Data migrations
  • Constraint names
  • Partial indexes
  • Complex index changes
  • Check constraints
  • Triggers and functions
# ❌ DANGEROUS: Autogenerated for column rename
def upgrade():
    op.drop_column("users", "name")      # DATA LOSS!
    op.add_column("users", sa.Column("full_name", sa.String()))

# ✅ CORRECT: Manual rename
def upgrade():
    op.alter_column("users", "name", new_column_name="full_name")

def downgrade():
    op.alter_column("users", "full_name", new_column_name="name")

Pattern: Safe Migration Structure

"""Add user preferences table.

Revision ID: abc123
Revises: def456
Create Date: 2024-01-15 10:30:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers
revision = "abc123"
down_revision = "def456"
branch_labels = None
depends_on = None


def upgrade() -> None:
    # Always explicit, never rely on defaults
    op.create_table(
        "user_preferences",
        sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
        sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
        sa.Column("theme", sa.String(50), nullable=False, server_default="light"),
        sa.Column("notifications_enabled", sa.Boolean(), nullable=False, server_default="true"),
        sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
    )

    # Explicit index names
    op.create_index(
        "ix_user_preferences_user_id",
        "user_preferences",
        ["user_id"],
    )

    # Foreign key with explicit name
    op.create_foreign_key(
        "fk_user_preferences_user_id",
        "user_preferences",
        "users",
        ["user_id"],
        ["id"],
        ondelete="CASCADE",
    )


def downgrade() -> None:
    # Always implement downgrade!
    op.drop_constraint("fk_user_preferences_user_id", "user_preferences", type_="foreignkey")
    op.drop_index("ix_user_preferences_user_id", "user_preferences")
    op.drop_table("user_preferences")

Pattern: Adding Non-Nullable Columns

Problem: Adding NOT NULL column to existing table fails if table has rows.

# ❌ WRONG: Fails if table has data
def upgrade():
    op.add_column("users", sa.Column("role", sa.String(50), nullable=False))

# ✅ CORRECT: Three-step process
def upgrade():
    # Step 1: Add as nullable
    op.add_column("users", sa.Column("role", sa.String(50), nullable=True))

    # Step 2: Backfill existing rows
    op.execute("UPDATE users SET role = 'member' WHERE role IS NULL")

    # Step 3: Add NOT NULL constraint
    op.alter_column("users", "role", nullable=False)

def downgrade():
    op.drop_column("users", "role")

Pattern: Data Migrations

Problem: Need to transform existing data during schema change.

from sqlalchemy import text

def upgrade():
    # Get connection for data operations
    connection = op.get_bind()

    # Add new column
    op.add_column("assessments", sa.Column("status", sa.String(20)))

    # Migrate data
    connection.execute(
        text("""
            UPDATE assessments 
            SET status = CASE 
                WHEN completed_at IS NOT NULL THEN 'completed'
                WHEN started_at IS NOT NULL THEN 'in_progress'
                ELSE 'pending'
            END
        """)
    )

    # Now safe to add NOT NULL
    op.alter_column("assessments", "status", nullable=False)


def downgrade():
    op.drop_column("assessments", "status")

Pattern: Large Table Migrations

Problem: Migrations on large tables can lock the table for too long.

def upgrade():
    # ✅ CORRECT: Add index concurrently (no lock)
    op.execute(
        "CREATE INDEX CONCURRENTLY ix_events_user_id ON events (user_id)"
    )

    # Note: CONCURRENTLY requires autocommit mode
    # Add to migration file:
    # from alembic import context
    # context.configure(transaction_per_migration=False)

def downgrade():
    op.execute("DROP INDEX CONCURRENTLY IF EXISTS ix_events_user_id")


# For column changes on large tables, consider:
# 1. Add new column (nullable)
# 2. Backfill in batches via separate script
# 3. Add constraint in separate migration

Pattern: Enum Changes

Problem: PostgreSQL enums are tricky to modify.

# Adding a value to existing enum
def upgrade():
    # PostgreSQL-specific: Add value to enum
    op.execute("ALTER TYPE assessment_status ADD VALUE 'archived'")

def downgrade():
    # Can't remove enum values in PostgreSQL!
    # Options:
    # 1. Leave it (usually fine)
    # 2. Recreate enum (complex, requires data migration)
    pass


# Creating new enum
def upgrade():
    # Create enum type first
    assessment_status = postgresql.ENUM(
        "draft", "active", "completed", "archived",
        name="assessment_status",
        create_type=True,
    )
    assessment_status.create(op.get_bind())

    # Then use it
    op.add_column(
        "assessments",
        sa.Column("status", assessment_status, nullable=False, server_default="draft"),
    )

def downgrade():
    op.drop_column("assessments", "status")
    op.execute("DROP TYPE assessment_status")

Pattern: Multiple Heads (Branching)

Problem: Multiple developers creating migrations simultaneously.

# Check for multiple heads
uv run alembic heads

# If multiple heads, create merge migration
uv run alembic merge -m "Merge heads" abc123 def456

# Or specify down_revision as tuple
down_revision = ("abc123", "def456")

Pattern: Testing Migrations

# test_migrations.py
import pytest
from alembic import command
from alembic.config import Config

@pytest.fixture
def alembic_config():
    config = Config("alembic.ini")
    return config

def test_upgrade_downgrade(alembic_config, test_db):
    """Test migrations can upgrade and downgrade."""
    # Upgrade to head
    command.upgrade(alembic_config, "head")

    # Downgrade to base
    command.downgrade(alembic_config, "base")

    # Upgrade again
    command.upgrade(alembic_config, "head")

def test_migration_has_downgrade():
    """Ensure all migrations have downgrade."""
    # Parse migration files and check downgrade isn't just 'pass'
    ...

Migration Review Checklist

Before applying any migration:

  • [ ] Downgrade function implemented (not just pass)
  • [ ] Column renames use alter_column, not drop+add
  • [ ] Non-nullable columns added with default or backfill
  • [ ] Large table operations consider locking
  • [ ] Indexes have explicit names
  • [ ] Foreign keys have explicit names and ON DELETE behavior
  • [ ] Enums created before use
  • [ ] Data migrations tested with real data volumes
  • [ ] Migration tested: upgrade, downgrade, upgrade

Production Safety

# Set statement timeout to prevent long locks
def upgrade():
    op.execute("SET statement_timeout = '5s'")

    # Your migration here

    op.execute("SET statement_timeout = '0'")  # Reset
# Always backup before production migrations
pg_dump -h host -U user -d dbname > backup_before_migration.sql

# Apply with --sql to preview
uv run alembic upgrade head --sql

# Apply for real
uv run alembic upgrade head

Common Issues

Issue Likely Cause Solution
"Target database is not up to date" Pending migrations Run alembic upgrade head
"Can't locate revision" Missing migration file Check version history
Multiple heads Concurrent development Create merge migration
Lock timeout Long-running migration Use CONCURRENTLY, batch updates
Data loss on deploy Column rename as drop+add Review autogenerated carefully