123 lines
4.6 KiB
Python
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")
|