"""backfill legacy owner user_id + stock_bans -> stock_bans_v2 (multi-tenant phase 1) Revision ID: 0011 Revises: 0010 Create Date: 2026-09-21 - legacy_owner = users 중 is_admin=True, id 최소인 사용자. - 없으면 스킵 (앱 기동 시 ensure_bootstrap_admin() 또는 관리자 API POST /auth/admin/tenant/migrate-legacy 가 동일 로직으로 재실행 가능 — idempotent). """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = "0011" down_revision = "0010" branch_labels = None depends_on = None _BACKFILL_TABLES = [ "portfolio", "trade_history", "active_trades", "trade_journals", "trading_sessions", "mcp_tool_calls", "claude_analysis_log", ] def _has_table(conn, table: str) -> bool: return conn.execute(sa.text( "SELECT 1 FROM information_schema.tables WHERE table_name=:t" ), {"t": table}).fetchone() is not None def upgrade() -> None: conn = op.get_bind() if not _has_table(conn, "users"): return owner_row = conn.execute(sa.text( "SELECT id FROM users WHERE is_admin = TRUE ORDER BY id LIMIT 1" )).fetchone() if owner_row is None: return # 관리자 미존재 — migrate-legacy API 또는 ensure_bootstrap_admin() 이 지연 백필 owner_id = owner_row[0] for table in _BACKFILL_TABLES: if not _has_table(conn, table): continue conn.execute(sa.text( f"UPDATE {table} SET user_id = :owner WHERE user_id IS NULL" ), {"owner": owner_id}) if _has_table(conn, "stock_bans") and _has_table(conn, "stock_bans_v2"): conn.execute(sa.text( """ INSERT INTO stock_bans_v2 (user_id, stock_code, consecutive_losses, ban_until, updated_at) SELECT :owner, stock_code, consecutive_losses, ban_until, updated_at FROM stock_bans ON CONFLICT (user_id, stock_code) DO NOTHING """ ), {"owner": owner_id}) def downgrade() -> None: # 데이터 백필 롤백은 위험(다른 정상 데이터와 구분 불가) — no-op. # 필요 시 수동으로 UPDATE ... SET user_id = NULL WHERE user_id = 실행. pass