Files
portfolio-hub/migrations/versions/20260502_0014_v1_api_schema.py
T

123 lines
4.6 KiB
Python

"""Add v1 API schema: portfolios.market, auth_tokens.runtime_id, broker_credentials, ledger_trades
Revision ID: 20260502_0014
Revises: 20260423_0013
Create Date: 2026-05-02 00:00:00
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
from sqlalchemy import inspect
revision = "20260502_0014"
down_revision = "20260423_0013"
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 _index_exists(inspector: sa.engine.Inspector, table_name: str, index_name: str) -> bool:
if not _table_exists(inspector, table_name):
return False
return any(idx["name"] == index_name for idx in inspector.get_indexes(table_name))
def upgrade() -> None:
bind = op.get_bind()
inspector = inspect(bind)
# 1. portfolios.market TEXT NULLABLE
if not _column_exists(inspector, "portfolios", "market"):
op.add_column("portfolios", sa.Column("market", sa.Text(), nullable=True))
# 2. auth_tokens.runtime_id TEXT UNIQUE NULLABLE
if not _column_exists(inspector, "auth_tokens", "runtime_id"):
op.add_column("auth_tokens", sa.Column("runtime_id", sa.Text(), nullable=True))
op.create_index(
"idx_auth_tokens_runtime_id_unique",
"auth_tokens",
["runtime_id"],
unique=True,
postgresql_where=sa.text("runtime_id IS NOT NULL"),
)
# 3. broker_credentials 테이블 신규
if not _table_exists(inspector, "broker_credentials"):
op.create_table(
"broker_credentials",
sa.Column("user_id", sa.Text(), nullable=False),
sa.Column("broker", sa.Text(), nullable=False),
sa.Column("account_ref", sa.Text(), nullable=False),
sa.Column("encrypted_payload", sa.Text(), nullable=False),
sa.Column("credential_version", sa.Integer(), nullable=False, server_default="1"),
sa.Column(
"last_rotated_at",
sa.TIMESTAMP(timezone=True),
nullable=False,
server_default=sa.text("NOW()"),
),
sa.PrimaryKeyConstraint("user_id", "broker"),
)
# 4. ledger_trades 테이블 신규
if not _table_exists(inspector, "ledger_trades"):
op.create_table(
"ledger_trades",
sa.Column(
"ledger_entry_id",
sa.BigInteger(),
sa.Identity(always=False),
nullable=False,
),
sa.Column("asset_id", sa.Text(), nullable=False),
sa.Column("order_id", sa.Text(), nullable=False),
sa.Column("execution_id", sa.Text(), nullable=False),
sa.Column("side", sa.Text(), nullable=False),
sa.Column("quantity", sa.Numeric(20, 8), nullable=False),
sa.Column("price", sa.Numeric(20, 8), nullable=False),
sa.Column("fee_amount", sa.Numeric(20, 8), nullable=False, server_default="0"),
sa.Column("tax_amount", sa.Numeric(20, 8), nullable=False, server_default="0"),
sa.Column("executed_at", sa.TIMESTAMP(timezone=True), nullable=False),
sa.Column(
"recorded_at",
sa.TIMESTAMP(timezone=True),
nullable=False,
server_default=sa.text("NOW()"),
),
sa.PrimaryKeyConstraint("ledger_entry_id"),
sa.UniqueConstraint("asset_id", "order_id", "execution_id", name="uq_ledger_trades_idempotency"),
)
op.create_index(
"idx_ledger_trades_asset_executed",
"ledger_trades",
["asset_id", "executed_at"],
)
def downgrade() -> None:
bind = op.get_bind()
inspector = inspect(bind)
if _table_exists(inspector, "ledger_trades"):
op.drop_table("ledger_trades")
if _table_exists(inspector, "broker_credentials"):
op.drop_table("broker_credentials")
if _column_exists(inspector, "auth_tokens", "runtime_id"):
if _index_exists(inspector, "auth_tokens", "idx_auth_tokens_runtime_id_unique"):
op.drop_index("idx_auth_tokens_runtime_id_unique", table_name="auth_tokens")
op.drop_column("auth_tokens", "runtime_id")
if _column_exists(inspector, "portfolios", "market"):
op.drop_column("portfolios", "market")