"""comparison_record: 唯一键 (user_id,trace_id) → trace_id 单列 + user_id 可空 Revision ID: comparison_record_trace_unique Revises: feedback_type_reply Create Date: 2026-07-03 12:00:00.000000 比价记录改「后端 harvest」:app-server 在帧0(pricebot 出 trace_id)即建行,随 done/finalize 逐步补全,客户端不再 POST 记录。故本表要两处调整: 1. user_id 改**可空**——harvest 建行那刻(软鉴权 / 老客户端匿名)可能还没有 user_id。 2. 唯一键 (user_id, trace_id) → **trace_id 单列**——trace_id 由 app-server 签发、全局唯一, 一次比价一行;harvest 建行时 user_id 还没有,不能再用复合键去重。 ⚠️ 上线前(QA)务必确认 comparison_record 无重复 trace_id:旧复合唯一键**允许**同 trace_id 跨不同 user_id(实际上客户端 UUID 从不重复,但约束没拦),若真有重复,建 trace 单列唯一会失败。 查: SELECT trace_id, COUNT(*) c FROM comparison_record GROUP BY trace_id HAVING c > 1; SQLite 不支持直接 drop/alter 约束/列,用 batch_alter_table(建临时表+拷数据+换名), 与 coupon_engage_per_package / store_mapping_* 同款;Postgres 直接执行原生 ALTER。 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = 'comparison_record_trace_unique' down_revision: Union[str, Sequence[str], None] = 'feedback_type_reply' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: with op.batch_alter_table('comparison_record', schema=None) as batch_op: # harvest 帧0 建行时 user_id 可能暂缺 → 放开非空。 batch_op.alter_column('user_id', existing_type=sa.Integer(), nullable=True) # 复合唯一 → trace_id 单列唯一。 batch_op.drop_constraint('uq_comparison_user_trace', type_='unique') batch_op.create_unique_constraint('uq_comparison_trace', ['trace_id']) def downgrade() -> None: with op.batch_alter_table('comparison_record', schema=None) as batch_op: batch_op.drop_constraint('uq_comparison_trace', type_='unique') batch_op.create_unique_constraint( 'uq_comparison_user_trace', ['user_id', 'trace_id'] ) # 回退非空前提是当时无 null user_id 行(harvest 期可能有孤儿行,回滚需先清理)。 batch_op.alter_column('user_id', existing_type=sa.Integer(), nullable=False)