"""add prediction status/time semantics and MatchStats provenance Revision ID: 0005_prediction_status_and_stats_provenance Revises: 0004_snapshot_and_constraints Create Date: 2026-09-15 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = '0005_prediction_status_and_stats_provenance' down_revision: Union[str, None] = '0004_snapshot_and_constraints' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # 1. Prediction 新增字段 op.add_column('predictions', sa.Column('status', sa.String(length=20), nullable=False, server_default='success')) op.add_column('predictions', sa.Column('match_kickoff_at', sa.DateTime(timezone=True), nullable=True)) op.add_column('predictions', sa.Column('prediction_created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now())) op.add_column('predictions', sa.Column('prediction_cutoff_at', sa.DateTime(timezone=True), nullable=True)) # 重命名 cutoff_at → 保留作为兼容,prediction_cutoff_at 为主字段 # op.drop_column('predictions', 'cutoff_at') # 暂不删除,避免破坏现有数据 # 2. 新增 status CHECK 约束 op.create_check_constraint('ck_status_enum', 'predictions', "status IN ('success', 'failed', 'degraded')") # 3. MatchStats 新增数据血缘字段 op.add_column('match_stats', sa.Column('source', sa.String(length=30), nullable=True)) op.add_column('match_stats', sa.Column('source_record_id', sa.String(length=100), nullable=True)) op.add_column('match_stats', sa.Column('retrieved_at', sa.DateTime(timezone=True), nullable=True)) op.add_column('match_stats', sa.Column('available_at', sa.DateTime(timezone=True), nullable=True)) # 4. 索引 op.create_index('ix_match_stats_available_at', 'match_stats', ['available_at']) op.create_index('ix_predictions_cutoff_at', 'predictions', ['prediction_cutoff_at']) def downgrade() -> None: op.drop_index('ix_predictions_cutoff_at', table_name='predictions') op.drop_index('ix_match_stats_available_at', table_name='match_stats') op.drop_column('match_stats', 'available_at') op.drop_column('match_stats', 'retrieved_at') op.drop_column('match_stats', 'source_record_id') op.drop_column('match_stats', 'source') op.drop_constraint('ck_status_enum', 'predictions', type_='check') op.drop_column('predictions', 'prediction_cutoff_at') op.drop_column('predictions', 'prediction_created_at') op.drop_column('predictions', 'match_kickoff_at') op.drop_column('predictions', 'status')