"""Backfill houses.address_fingerprint for legacy rows. Usage: cd tradein-mvp/backend DATABASE_URL=postgresql://... uv run python ../scripts/backfill_001_address_fingerprints.py Idempotent -- safe to re-run. Updates only rows with NULL address_fingerprint. Logs progress every 100 rows. """ from __future__ import annotations import logging import os import sys import psycopg # Allow running from repo root via `python tradein-mvp/scripts/...` sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend")) from app.services.matching.normalize import ( address_fingerprint, normalize_address, ) logging.basicConfig( format="%(asctime)s [%(levelname)s] %(message)s", level=logging.INFO, ) logger = logging.getLogger("backfill_001") def main() -> int: dsn = os.environ.get("DATABASE_URL") if not dsn: logger.error("DATABASE_URL env var required") return 1 # Strip SQLAlchemy dialect prefix if present (tradein-mvp uses postgresql+psycopg://) dsn = dsn.replace("postgresql+psycopg://", "postgresql://", 1) updated = 0 skipped = 0 with psycopg.connect(dsn) as conn: with conn.cursor() as cur: cur.execute( "SELECT id, address, lat, lon FROM houses " "WHERE address_fingerprint IS NULL AND address IS NOT NULL" ) rows = cur.fetchall() logger.info("Found %d houses missing fingerprint", len(rows)) for i, (house_id, address, lat, lon) in enumerate(rows, 1): normalized = normalize_address(address) if normalized is None: skipped += 1 continue fp = address_fingerprint(normalized, lat, lon) if fp is None: skipped += 1 continue cur.execute( "UPDATE houses SET address_fingerprint = %s WHERE id = %s", (fp, house_id), ) updated += 1 if i % 100 == 0: conn.commit() logger.info( "Progress: %d/%d updated=%d skipped=%d", i, len(rows), updated, skipped, ) conn.commit() logger.info( "Done: updated=%d skipped=%d total=%d", updated, skipped, updated + skipped ) return 0 if __name__ == "__main__": raise SystemExit(main())