"""asset_items — add ``disposal_photos`` JSON column. Revision ID: 591 Revises: 590 Create Date: 2026-09-02 处置(报废 / 出售 / 租赁归还 / 置换)常常是**唯一**留下凭证的时刻:回收 单、二手买家的付款截图、租赁商的归还回执、硬盘销毁照片。此前这些只能塞 进备注的自由文本里,等于没有。这一列存处置凭证图片的 S3 object key 列表 (与 ``asset_inventory_items.evidence_photos`` 同构):浏览器经 ``/api/v1/uploads/presign-put`` 直传,库里只存 key,读的时候现签一个 短时效 presigned GET。 JSON 列(而不是 TEXT/VARCHAR),让 MySQL/TiDB 在写入时就校验它是一个 JSON 数组 —— 驱动层绑错形状会当场报错,而不是留一条脏行。 不需要回填:历史处置本来就没有凭证,NULL 就是「没传过」的真值。 幂等 —— 重跑经 INFORMATION_SCHEMA 检查后是 no-op(TiDB 的 DDL 不在事务 里,迁移中途失败会留下「DDL 已生效但 alembic_version 没推进」)。 """ # ruff: noqa: I001 from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "591" down_revision = "590" branch_labels = None depends_on = None TABLE = "asset_items" COLUMN = "disposal_photos" def _column_exists(bind) -> bool: # noqa: ANN001 — alembic bind type is opaque rows = bind.execute( sa.text( "SELECT COUNT(*) FROM information_schema.COLUMNS " "WHERE TABLE_SCHEMA = DATABASE() " "AND TABLE_NAME = :t AND COLUMN_NAME = :c" ), {"t": TABLE, "c": COLUMN}, ).scalar() return bool(rows) def upgrade() -> None: bind = op.get_bind() if _column_exists(bind): return op.add_column(TABLE, sa.Column(COLUMN, sa.JSON(), nullable=True)) def downgrade() -> None: bind = op.get_bind() if not _column_exists(bind): return op.drop_column(TABLE, COLUMN)