"""bzzoiro 单一数据源重构:新增 standings、扩展 match_stats、删除 injuries Revision ID: 0015_bzzoiro_single_source Revises: 0014_predictions_agent_weights Create Date: 2026-09-20 变更内容: 1. 新增 standings 表(联赛积分榜快照,来源 bzzoiro /leagues/{id}/standings/) 2. match_stats 新增 bzzoiro /events/{id}/stats/ 扩展字段: home/away_big_chances, home/away_fouls 3. 删除 injuries 表(数据源已下线,不再采集伤停) 4. Understat 无独立表(xG 写入 match_stats),无需删表; 历史 source='understat' 数据保留,不再新增。 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = '0015_bzzoiro_single_source' down_revision: Union[str, None] = '0014_predictions_agent_weights' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # 0. matches 增加数据血缘列(bzzoiro 上游事件 ID) op.add_column('matches', sa.Column('source_event_id', sa.BigInteger())) op.create_index('ix_matches_source_event_id', 'matches', ['source_event_id']) # 1. standings 表 op.create_table( 'standings', sa.Column('id', sa.Integer(), primary_key=True), sa.Column('league_id', sa.Integer(), sa.ForeignKey('leagues.id'), nullable=False), sa.Column('season', sa.String(12), nullable=False), sa.Column('team_id', sa.Integer(), sa.ForeignKey('teams.id'), nullable=False), sa.Column('position', sa.Integer(), nullable=False), sa.Column('played', sa.Integer(), nullable=False, server_default='0'), sa.Column('won', sa.Integer(), nullable=False, server_default='0'), sa.Column('drawn', sa.Integer(), nullable=False, server_default='0'), sa.Column('lost', sa.Integer(), nullable=False, server_default='0'), sa.Column('goals_for', sa.Integer(), nullable=False, server_default='0'), sa.Column('goals_against', sa.Integer(), nullable=False, server_default='0'), sa.Column('goal_diff', sa.Integer(), nullable=False, server_default='0'), sa.Column('points', sa.Integer(), nullable=False, server_default='0'), sa.Column('xg_for', sa.Float()), sa.Column('xg_against', sa.Float()), sa.Column('form', sa.String(20)), sa.Column('zone', sa.String(50)), sa.Column('updated_at', sa.DateTime(timezone=True)), sa.Column('retrieved_at', sa.DateTime(timezone=True)), sa.UniqueConstraint('league_id', 'season', 'team_id', name='uq_standings_league_season_team'), ) op.create_index( 'ix_standings_league_season_pos', 'standings', ['league_id', 'season', 'position'], ) # 2. match_stats 扩展字段 op.add_column('match_stats', sa.Column('home_big_chances', sa.Integer())) op.add_column('match_stats', sa.Column('away_big_chances', sa.Integer())) op.add_column('match_stats', sa.Column('home_fouls', sa.Integer())) op.add_column('match_stats', sa.Column('away_fouls', sa.Integer())) # 3. 删除 injuries 表 op.drop_index('ix_injuries_player_fixture', table_name='injuries') op.drop_index('ix_injuries_team_date', table_name='injuries') op.drop_table('injuries') def downgrade() -> None: # 恢复 injuries 表(不含数据) op.create_table( 'injuries', sa.Column('id', sa.Integer(), primary_key=True), sa.Column('player_id', sa.Integer()), sa.Column('player_name', sa.String(120), nullable=False), sa.Column('team_id', sa.Integer(), sa.ForeignKey('teams.id')), sa.Column('fixture_id', sa.Integer()), sa.Column('league_id', sa.Integer()), sa.Column('injury_type', sa.String(50)), sa.Column('reason', sa.String(200)), sa.Column('injury_date', sa.Date()), sa.Column('return_date', sa.Date()), sa.Column('retrieved_at', sa.DateTime(timezone=True)), ) op.create_index('ix_injuries_player_fixture', 'injuries', ['player_id', 'fixture_id', 'injury_type']) op.create_index('ix_injuries_team_date', 'injuries', ['team_id', 'injury_date']) op.drop_index('ix_matches_source_event_id', table_name='matches') op.drop_column('matches', 'source_event_id') op.drop_column('match_stats', 'away_fouls') op.drop_column('match_stats', 'home_fouls') op.drop_column('match_stats', 'away_big_chances') op.drop_column('match_stats', 'home_big_chances') op.drop_index('ix_standings_league_season_pos', table_name='standings') op.drop_table('standings')