From a74d10ad2387f6dc2de1a0f24efa91af2b704004 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 9 Aug 2026 22:02:25 +0500 Subject: [PATCH] =?UTF-8?q?chore(tradein/db):=20=D1=81=D0=BD=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20DEPRECATED-=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD=D0=BA?= =?UTF-8?q?=D1=83=20listings.ceiling=5Fheight?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Хвост #2699. Миграция 238 свела высоту потолков к канону ceiling_height_m и намеренно оставила старую колонку: «сначала прод должен подтвердить, что колонку никто не пишет и не читает». Подтверждение получено. Критерий «писателей нет» был записан ДО работы: count(ceiling_height) обязан остаться 8552. Прод: 8552 07.08 (сразу после 238), 8552 09.08 16:54 UTC, 8552 в 16:57. Двое суток без единой записи. Контроль, что замер не мёртвый (иначе «ничего не изменилось» одинаково выглядит и при остановленном скрейпинге): за те же 2.5 минуты между двумя замерами count(ceiling_height_m) вырос 16133 → 16147, а last_seen_at строк с непустым ceiling_height обновлялся в минуту замера. UPDATE по этим самым строкам идут прямо сейчас и сносимую колонку не трогают. Потребители сверены на origin/main (не в рабочей копии): обращений к КОЛОНКЕ не осталось. В выдаче grep только имена полей датаклассов enrichment'ов, имя бинд-параметра `:ceiling_height`, который присваивается ceiling_height_m (yandex/detail.py:600), комментарии с историей и тесты, утверждающие отсутствие колонки в SQL писателей. Фронтовых вхождений нет. Зависимости в схеме проверены на проде, все нули: pg_depend по атрибуту, вьюхи/матвьюхи, индексы, CHECK, функции, тела обоих триггеров listings, publication column lists, foreign tables в gendesign-БД. CASCADE не нужен и не добавлен — в этом продукте DROP ... CASCADE уже терял гранты FDW-юзеру. Потери данных нет: строк «ceiling_height есть, канона нет» — 0; заполнены обе у 8552 строк, расхождений 0. lock_timeout обязателен и здесь дороже, чем у 250: удержание ACCESS EXCLUSIVE дёшево (DROP COLUMN не переписывает heap, только attisdropped в каталоге — в listings уже 4 таких пенька), но ожидание идёт по таблице в 19 GB, по которой постоянно пишет скрейпинг. Обе миграции прогнаны на одноразовом PostgreSQL 16.4 (postgis/postgis:16-3.4, --network none) на минимальном слепке схемы: применяются, идемпотентны при повторном прогоне, дубль индекса снят, колонка снята, ceiling_height_m и оба COMMENT на месте. Refs #2699 --- .../sql/251_listings_drop_ceiling_height.sql | 109 ++++++++++++++++++ .../backend/data/sql/_manifest_applied.txt | 1 + 2 files changed, 110 insertions(+) create mode 100644 tradein-mvp/backend/data/sql/251_listings_drop_ceiling_height.sql diff --git a/tradein-mvp/backend/data/sql/251_listings_drop_ceiling_height.sql b/tradein-mvp/backend/data/sql/251_listings_drop_ceiling_height.sql new file mode 100644 index 00000000..df6fcdb1 --- /dev/null +++ b/tradein-mvp/backend/data/sql/251_listings_drop_ceiling_height.sql @@ -0,0 +1,109 @@ +-- 251_listings_drop_ceiling_height.sql +-- Issue #2699 (хвост) — снос DEPRECATED-колонки listings.ceiling_height. +-- +-- Dependencies: 019_listings_alter_cian.sql (завела колонку, numeric(3,2)), +-- 238_listings_ceiling_height_unify.sql (перенесла значения в +-- канон ceiling_height_m и пометила эту колонку DEPRECATED). +-- Apply after: 240_trade_in_estimates_retain_until.sql +-- Deploy order: код УЖЕ впереди схемы — писатели сняты PR #2779 (07.08) и с тех +-- пор на проде. Это тот случай, когда «код первый» правилен: DROP COLUMN +-- безопасен только после того, как ни один живой writer/reader колонки не +-- остался. Обратный порядок (снести колонку, потом деплоить код) уронил бы +-- скрейпинг. +-- +-- ── ПОЧЕМУ ЭТО ОТДЕЛЬНЫЙ ФАЙЛ, А НЕ ЧАСТЬ 238 ─────────────────────────────── +-- 238 намеренно оставила колонку: «сначала прод должен подтвердить, что колонку +-- никто не пишет и не читает. Снос — отдельным шагом». Подтверждение получено, +-- ниже — числа. +-- +-- ── КРИТЕРИЙ «ПИСАТЕЛЕЙ НЕТ» (записан ДО, а не после) ─────────────────────── +-- count(ceiling_height) обязан остаться 8552 (8554 из #2699 минус 2 мусорных +-- значения cian, обнулённых шагом 1 миграции 238). +-- 2026-08-07 (сразу после 238): 8552 +-- 2026-08-09 16:54 UTC: 8552 +-- 2026-08-09 16:57 UTC: 8552 +-- Двое суток без единой записи. Контроль того, что замер не «мёртвый» (БД жива, +-- скрейпинг идёт, просто пишет в канон): за те же 2.5 минуты между двумя +-- замерами count(ceiling_height_m) вырос 16133 → 16147, а last_seen_at строк с +-- непустым ceiling_height обновлялся в ту же минуту, что и замер. То есть UPDATE +-- по этим строкам идут прямо сейчас и НЕ трогают сносимую колонку — это сильнее, +-- чем «два дня тишины». +-- +-- ── ПОТРЕБИТЕЛИ: сверка на origin/main перед сносом ───────────────────────── +-- `git grep -n 'ceiling_height\b' origin/main -- '*.py' '*.ts' '*.tsx' '*.sql'` +-- минус вхождения ceiling_height_m: ни одного обращения к КОЛОНКЕ не осталось. +-- Что попало в выдачу и почему это не потребители: +-- - имена полей Python-датаклассов enrichment'ов (CianEnrichment.ceiling_height, +-- YandexEnrichment.ceiling_height, YandexValuation...) — атрибуты объектов, +-- не колонки; +-- - `CAST(:ceiling_height AS numeric)` в yandex/detail.py:600 — ИМЯ БИНД- +-- ПАРАМЕТРА, а присваивается он колонке ceiling_height_m (соседняя строка); +-- то же в cian/detail.py (`:ch`) и base.py (`:ceiling_height_m`); +-- - комментарии/докстринги с историей #2699 и тесты, которые как раз +-- УТВЕРЖДАЮТ отсутствие колонки в SQL писателей +-- (tests/test_ceiling_height_unify_2699.py, test_scraper_admin_apis.py); +-- - 019/238 — сами миграции, их переписывать нельзя и не нужно. +-- Фронтовых (.ts/.tsx) вхождений нет вообще. +-- +-- ── ЗАВИСИМОСТИ В СХЕМЕ: проверено на проде 2026-08-09, все нули ──────────── +-- pg_depend по атрибуту listings.ceiling_height ................ 0 объектов +-- вьюхи/матвьюхи с 'ceiling' в определении ..................... 0 +-- индексы listings с 'ceiling' в indexdef ...................... 0 +-- CHECK/constraint с 'ceiling' ................................. 0 +-- функции и процедуры с 'ceiling_height' в теле ................ 0 +-- тела обоих триггеров listings (price_change, set_geom) ....... не упоминают +-- pg_publication_rel по listings (column list ломает DROP) ..... 0 (публикаций в БД нет) +-- foreign tables НА listings в gendesign-БД (FDW-читатель) ..... 0 +-- То есть CASCADE не нужен — и не должен появиться: в этом продукте +-- `DROP ... CASCADE` уже терял гранты FDW-пользователю (инцидент C3). +-- +-- ── ПОТЕРИ ДАННЫХ НЕТ (проверено, а не предположено) ──────────────────────── +-- строк, где ceiling_height IS NOT NULL AND ceiling_height_m IS NULL ..... 0 +-- строк, где заполнены обе ............................................ 8552 +-- из них расходятся значения .............................................. 0 +-- Всё содержимое сносимой колонки присутствует в каноне до последнего знака. +-- ceiling_height_m на момент написания: 16 147 непустых (после 238 было 15 591 — +-- канон растёт, то есть живой). +-- +-- ── СТОИМОСТЬ БЛОКИРОВКИ И ПОЧЕМУ SET LOCAL lock_timeout ─────────────────── +-- ALTER TABLE ... DROP COLUMN берёт ACCESS EXCLUSIVE на listings. УДЕРЖАНИЕ +-- дёшево и не зависит от размера таблицы: PostgreSQL не переписывает heap, а +-- помечает атрибут attisdropped в каталоге (в listings уже 4 таких «пенька» от +-- прошлых сносов при 92 живых колонках) — единицы миллисекунд. +-- +-- Дорого ОЖИДАНИЕ выдачи лока, и цена здесь выше, чем у 250: listings — 19 GB, +-- 97 540 строк, по ней постоянно идёт скрейпинг (в т.ч. длинные проходы вроде +-- avito_full_load). Ждущий ACCESS EXCLUSIVE встаёт в очередь ПЕРЕД новыми +-- запросами, поэтому за ним начнут ждать обычные SELECT/UPDATE приложения — +-- ровно то, что 2026-08-07 положило деплой на 29 минут (#2791, #2792). +-- Поэтому `SET LOCAL lock_timeout = '5s'` (снизу ограничено deadlock_timeout = +-- 1 s на проде, сверху — потолок простоя очереди приложения; на работу ПОД +-- локом не влияет). Срабатывание = честный красный деплой через 5 секунд, +-- миграция не помечается применённой, повторить в окно потише. +-- +-- IDEMPOTENCY / SAFETY: +-- - DROP COLUMN IF EXISTS — безопасный re-run. +-- - Без CASCADE: зависимых объектов нет (см. выше), а CASCADE молча снёс бы +-- то, что появится позже. +-- - Одна DDL-операция внутри BEGIN/COMMIT. +-- - Откат: колонку вернуть можно (ALTER TABLE ... ADD COLUMN), но данные в неё +-- не восстановятся — они и не нужны, дубликат канона (0 расхождений). +-- +-- Критерий приёмки (записан ДО применения): +-- 1. Запись в _schema_migrations по имени этого файла (а не «деплой зелёный»). +-- 2. information_schema.columns по listings: ceiling_height отсутствует, +-- ceiling_height_m на месте и count(ceiling_height_m) >= 16 147. +-- 3. Скрейпинг продолжает писать: count(ceiling_height_m) растёт после сноса. + +BEGIN; + +-- Ограничивает ОЖИДАНИЕ лока, не работу под ним. Обоснование значения — в шапке +-- и в .claude/rules/sql.md § lock_timeout. +SET LOCAL lock_timeout = '5s'; + +ALTER TABLE listings DROP COLUMN IF EXISTS ceiling_height; + +COMMENT ON COLUMN listings.ceiling_height_m IS + 'Высота потолков, метры. ЕДИНСТВЕННАЯ колонка этого признака (#2699): дубль listings.ceiling_height (019) снесён миграцией 251 после того, как 238 перенесла в неё значения. Пишут все источники через scraper_kit.ceiling_height.plausible_ceiling_m (гейт правдоподобия 2.0-6.0 м). Читает estimator (comp-scoring #2012).'; + +COMMIT; diff --git a/tradein-mvp/backend/data/sql/_manifest_applied.txt b/tradein-mvp/backend/data/sql/_manifest_applied.txt index d011b41a..80028a39 100644 --- a/tradein-mvp/backend/data/sql/_manifest_applied.txt +++ b/tradein-mvp/backend/data/sql/_manifest_applied.txt @@ -243,3 +243,4 @@ 234_scrape_runs_ban_kind_unknown.sql 240_trade_in_estimates_retain_until.sql 250_drop_duplicate_expires_at_index.sql +251_listings_drop_ceiling_height.sql -- 2.45.3