"""Backfill `scene_performers.source_id` dla wierszy sprzed migracji 0026. Wnioskowanie jest pewne tylko dla scen, które mają `scene_external_refs` z DOKŁADNIE jednego źródła: skoro tylko ono kiedykolwiek dotknęło tę scenę, to cała jej obsada stamtąd pochodzi. Takich scen jest 3,39 mln i pokrywają 4,15 mln przypisań, czyli ~96% tabeli. Sceny wieloźródłowe (67 tys.) zostawiamy z NULL-em — tam nie da się powiedzieć, które źródło przypisało którego performera, a zgadywanie zepsułoby cały sens kolumny. Granularność jest na poziomie ŹRÓDŁA, nie pojedynczego tuba: wszystkie direct-scrapery dzielą jeden rekord `sources` (`SCRAPER_SOURCE_NAME`). To wystarcza do pytania, które było nie do zadania wcześniej: „czy tę obsadę potwierdził kanon, czy dokleił ją tube-search". Wsadowo i z osobną transakcją na paczkę — pojedynczy UPDATE na 4 mln wierszy trzymałby locki na `scene_performers` przez cały czas i zablokował ingest (zdarzyło się przy migracji 0026: porzucone zapytanie analityczne wstrzymało ALTER TABLE, a za nim ustawiła się kolejka). Uruchomienie (kontener worker): python -m scripts.backfill_scene_performer_source # dry-run python -m scripts.backfill_scene_performer_source --yes # wykonaj """ from __future__ import annotations import argparse import logging from sqlalchemy import text from app.db import session_scope log = logging.getLogger(__name__) _BATCH = 50_000 _COUNT_SQL = """ SELECT count(*) FROM scene_performers sp WHERE sp.source_id IS NULL AND (SELECT count(DISTINCT r.source_id) FROM scene_external_refs r WHERE r.scene_id = sp.scene_id) = 1 """ # ctid-based batching: bez sztucznego klucza na (scene_id, performer_id) to najtańszy # sposób na „weź N dowolnych jeszcze niewypełnionych". _UPDATE_SQL = """ UPDATE scene_performers sp SET source_id = ( SELECT (array_agg(DISTINCT r.source_id))[1] FROM scene_external_refs r WHERE r.scene_id = sp.scene_id ) WHERE sp.ctid IN ( SELECT sp2.ctid FROM scene_performers sp2 WHERE sp2.source_id IS NULL AND (SELECT count(DISTINCT r.source_id) FROM scene_external_refs r WHERE r.scene_id = sp2.scene_id) = 1 LIMIT :batch ) """ def main() -> None: logging.basicConfig(level=logging.INFO, format="%(message)s") ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--yes", action="store_true", help="wykonaj (bez tego dry-run)") ap.add_argument("--batch", type=int, default=_BATCH) args = ap.parse_args() with session_scope() as s: total = int(s.execute(text(_COUNT_SQL)).scalar_one()) log.info("do wypełnienia: %d przypisań (sceny jednoźródłowe)", total) if not args.yes: log.info("(dry-run — uruchom z --yes)") return done = 0 while True: with session_scope() as s: n = int(s.execute(text(_UPDATE_SQL), {"batch": args.batch}).rowcount or 0) s.commit() if n == 0: break done += n log.info(" wypełnione %d / %d", done, total) log.info("GOTOWE: %d wierszy", done) if __name__ == "__main__": main()