Files
Profeto/alembic/versions/0005_prediction_status_and_stats_provenance.py
shangfangjian 0980c2242a feat: P0-02/P0-03/P0-05 数据库时间语义与约束
P0-02: MatchStats 增加 source/source_record_id/retrieved_at/available_at
        context_builder 过滤统计数据时检查 available_at <= cutoff
P0-03: Prediction 增加 match_kickoff_at/prediction_created_at/prediction_cutoff_at
        明确区分比赛时间/预测创建时间/数据截止时间
P0-05: Prediction 增加 status 字段(success/failed/degraded)
        数据库 CHECK 约束

Alembic: 0005_prediction_status_and_stats_provenance

P1-02: injuries as_of 不再截断为 date,保持 datetime 精度
2026-09-15 02:04:25 +08:00

59 lines
2.6 KiB
Python

"""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')