"""P0-01: 比分可信度——score_status + 允许完赛缺分(NULL,禁止伪造 0:0) 替换 ck_matches_finished_has_score:引入 score_status(known/missing/unknown), 完赛 + score_status=missing 时 home/away_goals 必须 NULL(不伪造比分)。 Revision ID: 0022_match_score_status Revises: 0021_match_source_event_id_unique Create Date: 2026-09-22 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa revision: str = '0022_match_score_status' down_revision: Union[str, None] = '0021_match_source_event_id_unique' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # 1) 新增 score_status 列(默认 unknown) op.add_column( 'matches', sa.Column('score_status', sa.String(20), server_default='unknown', nullable=False), ) # 2) 按现有数据回填 score_status(绝不写 goals=0): # - 有比分(两列均非 NULL) → known # - 无比分 + 完赛 → missing(缺分) # - 其余 → unknown op.execute( "UPDATE matches SET score_status = 'known'" " WHERE home_goals IS NOT NULL AND away_goals IS NOT NULL" ) op.execute( "UPDATE matches SET score_status = 'missing'" " WHERE match_status = 'finished' AND home_goals IS NULL AND away_goals IS NULL" ) # 3) 删除旧约束,加新约束 op.drop_constraint('ck_matches_finished_has_score', 'matches', type_='check') op.create_check_constraint( 'ck_matches_score_status_enum', 'matches', "score_status IN ('known', 'missing', 'unknown')", ) op.create_check_constraint( 'ck_matches_score_integrity', 'matches', "match_status <> 'finished'" " OR (score_status = 'known' AND home_goals IS NOT NULL AND away_goals IS NOT NULL)" " OR (score_status IN ('missing', 'unknown') AND home_goals IS NULL AND away_goals IS NULL)", ) def downgrade() -> None: op.drop_constraint('ck_matches_score_integrity', 'matches', type_='check') op.drop_constraint('ck_matches_score_status_enum', 'matches', type_='check') op.create_check_constraint( 'ck_matches_finished_has_score', 'matches', "match_status <> 'finished' OR (home_goals IS NOT NULL AND away_goals IS NOT NULL)", ) op.remove_column('matches', 'score_status')