listings_search_mv с 050 отдавала distance_to_metro_m, last_price_change и
photos_count литеральным NULL: имена зарезервировали, реализацию не подключили
никогда. Читателей ноль — `git grep` по origin/main даёт 8 попаданий, и все
восемь это сами файлы 050/094; SELECT * по витрине в репозитории нет ни одного,
рефлексии тоже, единственный читатель (services/search_query.py) перечисляет
27 колонок явно и ни одну из трёх не просит. Правок кода миграция не требует.
district оставлен намеренно: он объявлен в schemas/search_response.py, то есть
API его отдаёт, и снос — ломающее изменение контракта, решение владельца.
Материализованному представлению нельзя удалить колонку (проверено на
одноразовой БД: ALTER MATERIALIZED VIEW и ALTER TABLE одинаково отвечают «not
supported for materialized views»), поэтому пересоздание — как в 094. Без
CASCADE: зависимых объектов на проде ноль, а появись зависимость — деплой обязан
покраснеть, а не снести её молча.
Гранты снимаются и переигрываются в той же транзакции, а не переносятся руками:
DROP уносит ACL, и ручной слепок протухает молча, если файл пролежит до деплоя.
Сегодня переигрывать нечего (relacl витрины — только владелец), но первая
редакция блока теряла колоночные гранты — это поймал прогон на одноразовой БД,
не рассуждение, и снимок теперь берёт и pg_attribute.attacl.
Все 6 индексов воссоздаются, включая UNIQUE по listing_id — без него ночной
REFRESH ... CONCURRENTLY молчит до самой ночи, а потом падает.
Тест собирает список колонок разбором актуального определения витрины и
краснеет на составе списка, а не на подстроке: переформатирование SQL его не
трогает, возврат колонки — трогает. Отдельно сверяет, что API не просит у
витрины колонок, которых в ней нет.
Refs #2857