"""Backfill Customer.add_time from raw_json.addTime (no re-scrape). Iterates existing customer rows, parses raw_json.addTime (str or epoch) → add_time ms-epoch, updates in batches. Idempotent: skips rows where add_time is already set. WeChat rows have addTime; WhatsApp rows do not (createTime is owner-staff, not customer-add) — those stay NULL. """ import asyncio import time from sqlalchemy import select, update from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from app.db import models # noqa: F401 from app.db.models import Customer from app.scraper.wechat import _parse_add_time BATCH = 200 async def main(db_url: str = "sqlite+aiosqlite:///./smoke.db") -> None: engine = create_async_engine(db_url) sm = async_sessionmaker(engine, expire_on_commit=False) done = 0 async with sm() as s: rows = ( await s.execute(select(Customer.friend_id, Customer.raw_json, Customer.add_time)) ).all() for fid, raw, existing in rows: if existing or not raw: continue val = _parse_add_time(raw.get("addTime")) if val is None: continue await s.execute( update(Customer).where(Customer.friend_id == fid).values(add_time=val) ) done += 1 if done % BATCH == 0: await s.commit() print(f"[{time.strftime('%H:%M:%S')}] {done} updated...") await s.commit() print(f"DONE — {done} customers got add_time") await engine.dispose() if __name__ == "__main__": asyncio.run(main())