_upsert_prediction 改为 _insert_or_find_by_fingerprint: - 同 input_hash → 返回已有行(绝不 UPDATE pred_/reasoning/agent_outputs) - 不同 input_hash → INSERT 新行 input_hash 升级为规范 JSON SHA-256,捕获:match_id, cutoff, prompt_version, prompt_hash, system_prompt_hash, provider, model, mode, run_type, temperature, context_hash, agent_ids。移除旧 (match, provider, model, mode, run_type) 唯一约束, 改为 partial unique index(WHERE input_hash IS NOT NULL,兼容旧 NULL 数据)。 三条路径(single/multi/baseline)统一传足指纹字段。 迁移 0024 + 测试 test_p0_prediction_fingerprint(10/10);全量 295 通过。
42 lines
1.4 KiB
Python
42 lines
1.4 KiB
Python
"""P0-03: Prediction 幂等指纹——移除旧唯一约束,改为 partial unique on input_hash
|
|
|
|
input_hash 非空时唯一(同指纹返回已有行,不 UPDATE/INSERT);
|
|
兼容旧数据 NULL input_hash(不强制回填)。
|
|
|
|
Revision ID: 0024_prediction_idempotent_fingerprint
|
|
Revises: 0023_standings_append_only
|
|
Create Date: 2026-09-22
|
|
"""
|
|
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
revision: str = '0024_prediction_idempotent_fingerprint'
|
|
down_revision: Union[str, None] = '0023_standings_append_only'
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# 移除旧唯一约束(match, provider, model, mode, run_type)
|
|
op.drop_constraint(
|
|
'uq_predictions_match_provider_model_mode_run_type',
|
|
'predictions', type_unique=True,
|
|
)
|
|
# P0-03: partial unique on input_hash(非空时唯一)
|
|
op.create_index(
|
|
'ix_predictions_input_hash_unique', 'predictions', ['input_hash'], unique=True,
|
|
postgresql_where=sa.text('input_hash IS NOT NULL'),
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index('ix_predictions_input_hash_unique', table_name='predictions')
|
|
op.create_unique_constraint(
|
|
'uq_predictions_match_provider_model_mode_run_type',
|
|
'predictions',
|
|
['match_id', 'provider', 'model', 'mode', 'run_type'],
|
|
)
|