CI (там живой postgres, и эти два теста реально исполняются) поймал
ValidationError: ScrapedLot.phones — jsonb вида
[{countryCode, number, type}, ...] (019_listings_alter_cian.sql:41), а я
передавал ["+79000000000"].
Заодно проверено конструирование лота напрямую, без БД — это ловит любые
pydantic-ошибки, не дожидаясь прогона CI. Всплыла деталь, подтверждающая
разбор в самом фиксе: у бедного лота phones не None, а ПУСТОЙ СПИСОК.
Сборка параметров (`_to_json(lot.phones) if lot.phones else None`)
схлопывает его в None, то есть в SQL приходит NULL и COALESCE удерживает
прежнее значение. Если бы пустой список доезжал как '[]', фикс для phones
и metro_stations молча не работал бы.
Refs #3063
CI поймал то, что локально не проверилось: ruff в этом окружении вызывался
не тем интерпретатором и молча не запускался. strict=True здесь не просто
успокаивает линтер — равенство длин кортежей уже проверено assert'ом выше,
так что строгий zip выражает ровно то же намерение явно.
Refs #3063
Апсерт listings присваивал шесть полей напрямую (= EXCLUDED.X): phones,
is_homeowner, is_pro_seller, bargain_allowed, sale_type, metro_stations.
Комментарий рядом («Cian-specific: обновляем при каждом re-scrape») был
верен, пока в listings писал один cian, который отдаёт эти поля на каждом
проходе. Сейчас в ту же таблицу пишут yandex/avito/domklik, чей SERP их не
отдаёт вовсе — и NULL молча затирал значение, добытое detail-обогащением.
Ни ошибки, ни лога, ни изменения статуса прогона: колонка просто
откатывалась к NULL на обычном переобходе.
Прод 23-24.08, yandex: is_homeowner NULL у 98.6% (16 232 из 16 460)
активных листингов, is_pro_seller — у 76.6%.
Соседние поля в том же операторе уже защищены COALESCE ровно от этого, с
явными объяснениями #2007 / #2594 / #2777 — address, city, kitchen_area_m2,
ceiling_height_m, mortgage_available, is_apartments, is_rosreestr_checked.
Эти шесть в защиту просто не попали.
Правка в ДВУХ местах, не в одном. Кроме SET изменена правая часть гейта
`IS DISTINCT FROM` (#2992): он решает, переписывать ли строку, сравнивая
текущие значения с ИТОГОВЫМИ (post-COALESCE). Оставить его на сыром
EXCLUDED значило бы, что гейт видит «NULL против значения» и считает
строку изменившейся там, где она не меняется — вернулись бы ровно те
лишние UPDATE и TOAST-чанки, ради которых #2992 делался.
Пустые phones/metro_stations сериализуются как None (`_to_json(...) if
lot.X else None`), то есть приходят SQL NULL — COALESCE их удерживает, а
не подменяет пустым массивом.
Тест сторожит и паритет тоже: сравнивает оба кортежа гейта поэлементно,
поэтому поймает будущее расхождение по ЛЮБОЙ колонке, не только по этим
шести.
Проверено:
- фальсификация: все 7 тестов падают на коде до правки
- 770 passed, 10 skipped (-k "upsert or listing or scrape or writer or
snapshot or timestamps")
- выравнивание кортежей гейта: 39 = 39 элементов, порядок колонок совпал
Из #3063 сделан только пункт 1. Пункт 2 (выводить is_pro_seller из
agency_name) НЕ вошёл — см. комментарий в issue: у него есть риск ложного
срабатывания, которого нет у этой правки.
Refs #3063