listings_snapshots не имел ни одного writer'а — таблица оставалась пустой.
offer_price_history уже писался в save_detail_enrichment, но ON CONFLICT DO NOTHING
не указывал имя constraint'а (добавлено в 047_cian_history_sanitize.sql), что делало
де-дупликацию неэффективной.
Добавлено:
- snapshot_writer.py: upsert_listing_snapshot() — атомарный INSERT ... ON CONFLICT
DO UPDATE на (listing_id, snapshot_date), без commit (commit у caller'а).
- base.py save_listings(): вызывает upsert_listing_snapshot после каждого INSERT/UPDATE
listings; принимает опциональный run_id; ошибка snapshot не прерывает батч.
- cian_detail.py save_detail_enrichment(): SELECT price_rub/price_per_m2 из listings,
затем upsert_listing_snapshot; ON CONFLICT для offer_price_history теперь ссылается
на offer_price_history_listing_change_uq (именованный constraint из мигр. 047).
- test_snapshot_writer.py: 17 unit-тестов покрывают SQL-форму, params, идемпотентность,
интеграцию с save_listings и save_detail_enrichment, fault-tolerance.