"""comparison_record 加 product_names 列(下单商品名派生串)+ 回填历史行 admin 比价记录页要把原「店/商品」一列拆成「店」+「商品」两列、并支持按商品名搜索。 items 是 JSON(SQLite 下 json.dumps ensure_ascii 会把中文转义存成 \\uXXXX,无法直接 CAST+LIKE 命中中文),故把商品名派生成普通文本列,跨库 LIKE 一致、可索引。 写路径(upsert_record / harvest_done)已同步派生;本迁移建列并从已存 items 回填历史行。 全新环境顺序应用即得空列 + 回填(表本为空,回填 no-op)。 Revision ID: comparison_product_names Revises: comparison_record_trace_unique Create Date: 2026-07-04 00:00:00.000000 """ import json from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "comparison_product_names" down_revision: str | Sequence[str] | None = "comparison_record_trace_unique" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def _product_names(items) -> str | None: """items([{name,...}]) → 顿号分隔的商品名串(去重保序)。与 repositories.comparison ._product_names_from_items 同口径,迁移内联一份避免耦合 app 代码。""" if isinstance(items, str): # SQLite: items 以 JSON 文本存,取回是 str try: items = json.loads(items) except (ValueError, TypeError): return None if not isinstance(items, list) or not items: return None names: list[str] = [] for it in items: name = it.get("name") if isinstance(it, dict) else None if not name: continue s = str(name).strip() if s and s not in names: names.append(s) joined = "、".join(names) return joined[:500] or None def upgrade() -> None: with op.batch_alter_table("comparison_record", schema=None) as batch_op: batch_op.add_column(sa.Column("product_names", sa.String(length=512), nullable=True)) # 回填:从已存 items 派生商品名。items 小(菜名列表),一次取回即可;只更新有商品名的行。 bind = op.get_bind() rows = bind.execute( sa.text("SELECT id, items FROM comparison_record") ).fetchall() for rid, items in rows: pn = _product_names(items) if pn: bind.execute( sa.text("UPDATE comparison_record SET product_names = :pn WHERE id = :id"), {"pn": pn, "id": rid}, ) def downgrade() -> None: with op.batch_alter_table("comparison_record", schema=None) as batch_op: batch_op.drop_column("product_names")