feat: BAT-025 add portfolio ledger manual metadata migration

This commit is contained in:
2026-07-15 17:58:18 +09:00
committed by Hermes Agent
parent 600375beca
commit 1909caa22c
4 changed files with 407 additions and 0 deletions
@@ -0,0 +1,96 @@
"""portfolio_ledger: manual ledger metadata columns backfill-safe upgrade
Revision ID: 20260715_0028
Revises: 20260628_0027
Create Date: 2026-07-15 00:00:00
This migration adds the portfolio_ledger additive metadata columns introduced by
REQ-056. It is safe for both fresh DBs and legacy DBs where ensure_schema() already
created columns at runtime.
- source: TEXT NOT NULL DEFAULT 'broker'
- target_ledger_entry_id: BIGINT NULL
- confirmation_evidence: TEXT NULL
- actor_user_id: TEXT NULL
Runtime behavior:
- if source exists, backfill NULL -> 'broker', then enforce NOT NULL/default.
- if source/other columns are absent, add them as idempotent ALTERs.
- existing rows are preserved; only metadata defaults/backfill are adjusted.
- downgrade is forward-only (columns are not removed).
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
from sqlalchemy import inspect, text
revision = "20260715_0028"
down_revision = "20260628_0027"
branch_labels = None
depends_on = None
def _table_exists(inspector: sa.engine.Inspector, table_name: str) -> bool:
return table_name in inspector.get_table_names()
def _column_exists(inspector: sa.engine.Inspector, table_name: str, column_name: str) -> bool:
if not _table_exists(inspector, table_name):
return False
return any(c["name"] == column_name for c in inspector.get_columns(table_name))
def upgrade() -> None:
bind = op.get_bind()
inspector = inspect(bind)
if not _table_exists(inspector, "portfolio_ledger"):
return
if not _column_exists(inspector, "portfolio_ledger", "source"):
op.add_column(
"portfolio_ledger",
sa.Column(
"source",
sa.Text(),
nullable=False,
server_default=sa.text("'broker'"),
),
)
else:
# runtime-precreated 환경: source가 NULL이면 명시적으로 backfill 후 NOT NULL + 기본값 보장
bind.execute(text("UPDATE portfolio_ledger SET source = 'broker' WHERE source IS NULL"))
op.alter_column(
"portfolio_ledger",
"source",
existing_type=sa.Text(),
server_default=sa.text("'broker'"),
nullable=False,
)
if not _column_exists(inspector, "portfolio_ledger", "target_ledger_entry_id"):
op.add_column(
"portfolio_ledger",
sa.Column("target_ledger_entry_id", sa.BigInteger(), nullable=True),
)
if not _column_exists(inspector, "portfolio_ledger", "confirmation_evidence"):
op.add_column(
"portfolio_ledger",
sa.Column("confirmation_evidence", sa.Text(), nullable=True),
)
if not _column_exists(inspector, "portfolio_ledger", "actor_user_id"):
op.add_column(
"portfolio_ledger",
sa.Column("actor_user_id", sa.Text(), nullable=True),
)
# Forward-only policy: 운영 rollback은 컬럼 삭제/재적용 경로가 아니다.
def downgrade() -> None:
"""Forward-only migration: 컬럼 삭제 downgrade는 수행하지 않는다."""
return