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 精度
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
"""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')
|
||||
Reference in New Issue
Block a user