gendesign/tradein-mvp/backend/data/sql/214_drop_dead_run_metrics.sql
bot-backend 3c5f535e6c
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m2s
CI Trade-In / backend-tests (pull_request) Successful in 2m58s
fix(tradein/admin): гейт отмены по источнику, честный комментарий view, лимит 50 (#2674)
Ревью PR #2684 — четыре MINOR.

1. Починка фильтра открыла кнопку отмены на все 53 источника. Раньше таблица была
   пуста на каждой вкладке, поэтому кнопка не рендерилась НИ РАЗУ и дыра не
   проявлялась: ручки отмены source не проверяют вовсе. Оператор на вкладке Авито
   мог бы «отменить» refresh_search_matview — задача продолжила бы работать под
   статусом 'cancelled' (ещё один врущий статус ровно в тот день, когда их
   вычищаем), а has_running_run перестал бы держать single-run guard, который
   существует из-за инцидента с двойным свипом и баном (2026-05-31).

   Гейт поставлен на общем узле всех пяти ручек — scrape_runs.honors_cancel +
   отказ в mark_cancelled, — а не в UI: иначе ручной POST по-прежнему снимал бы
   guard. Флаг cancellable отдаётся в строке, UI по нему прячет кнопку.
   Состав набора выведен из call-site'ов runs.is_cancelled: city-sweep'ы (все
   площадки и города), full-load'ы, avito_newbuilding_sweep, rosreestr_dkp_import.
   Правило НЕ «любой *_sweep»: yandex_newbuilding_sweep отмену не опрашивает.

2. Комментарий пересозданного v_data_quality утверждал, что его обновляет
   /api/v1/admin/data-quality. Читателей у view нет ни одного — живая ручка строит
   свой запрос. PR с тезисом «ложный показатель хуже отсутствующего» не имеет права
   переносить в прод ложное утверждение о читателе.

3. Лимит выдачи 20 → 50: первые 20 строк по started_at на три четверти —
   сердцебиение proxy_healthcheck (1631 из 3245), часовой сбор мог не поместиться.
   Привязка к вкладке НЕ возвращается.

4. Тест «действующее определение view» искал маркер подстрокой с OR REPLACE —
   миграция с обычным CREATE VIEW или парой DROP+CREATE была бы невидима, и тест
   проверял бы 214, пока показатель уже вернулся. Заменено регуляркой на обе формы.

Фальсификация трёх новых тестов патч-методом — все три красные. Полный прогон
3490 passed / 9 skipped, tsc --noEmit чистый.
2026-08-06 03:57:46 +05:00

116 lines
8.5 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 214_drop_dead_run_metrics.sql
-- Purpose (#2674): удалить показатели, которые не могут быть ненулевыми.
--
-- Один класс находок: колонка есть, админка её показывает, писателя нет ни одного —
-- и ноль читается как «всё чисто», а не как «мы это не считаем». Такой показатель
-- хуже отсутствующего: он создаёт ложную уверенность. Числа с прода 2026-08-06.
--
-- 1. listings.is_outlier + v_data_quality.outliers_flagged
-- 93 408 строк listings, is_outlier = false у ВСЕХ, NULL у нуля — то есть
-- только DEFAULT из 002, ни одного UPDATE за всю историю. Писателя нет и не
-- задумывалось: «выброс» у эстиматора вычисляется Tukey-фильтром по КОНКРЕТНОЙ
-- подборке аналогов (estimator._filter_outliers) и живёт ровно один запрос —
-- один и тот же лот выброс для одной оценки и нормальный аналог для соседней.
-- Persist-флаг на объявлении такое отношение выразить не может в принципе,
-- поэтому реализовать «пометку» нечем: это остаток отменённой затеи
-- («помечено модели после aggregation», 002_core_tables.sql), а не недоделка.
--
-- 2. scrape_runs.run_type
-- 3244 прогона, ровно одно значение 'city_sweep' — DEFAULT из 051; ни одно
-- место кода run_type не задаёт. Колонка подписывала «city_sweep» прогоны,
-- которые никаким sweep не были: proxy_healthcheck (1630 шт.),
-- deactivate_stale_*, sber_index_pull, rosreestr_dkp_import. Что именно бежало,
-- честно называет source — второй, вырожденной оси не нужно.
--
-- 3. scrape_runs.http_requests / http_errors / returning_count / disappeared_count
-- Ноль во всех 3244 прогонах с миграции 015, писателя нет вообще.
-- HTTP-запросы не считает никто ни в одном фетчере — заполнить нечем без
-- сквозной инструментации всех путей. Ошибки и «пропало/вернулось» УЖЕ
-- считает тот, кто их знает, и кладёт в counters jsonb: errors_count у
-- pipeline, deactivated/revived у deactivate_stale_*. Отдельные колонки были
-- бы вторым, иначе определённым счётчиком того же слова.
--
-- ⚠️ View-зависимость: v_data_quality (последний DDL — 095_dead_schema.sql) читает
-- listings.is_outlier и содержит CTE `SELECT * FROM listings`, что фиксирует
-- column-level зависимость на все колонки. Порядок тот же, что в 095:
-- DROP VIEW → DROP COLUMN → CREATE VIEW (уже без outliers_flagged).
--
-- Dependencies: 002_core_tables.sql, 015_scrape_runs.sql, 051_scrape_runs_extend.sql,
-- 095_dead_schema.sql (последний DDL v_data_quality).
-- Идемпотентно: DROP VIEW IF EXISTS / DROP COLUMN IF EXISTS / CREATE OR REPLACE VIEW.
BEGIN;
-- ── 1. is_outlier: снять зависимый view, дропнуть колонку, пересоздать view ───
DROP VIEW IF EXISTS v_data_quality;
ALTER TABLE IF EXISTS listings DROP COLUMN IF EXISTS is_outlier;
-- DDL идентичен 095, минус строка outliers_flagged (см. п.1 шапки).
CREATE OR REPLACE VIEW v_data_quality AS
WITH active_listings AS (
SELECT * FROM listings WHERE is_active = true
)
SELECT
(SELECT count(*) FROM houses) AS houses_total,
(SELECT count(*) FROM houses h
WHERE EXISTS (SELECT 1 FROM house_sources hs WHERE hs.house_id = h.id)) AS houses_with_source,
(SELECT count(*) FROM houses h
WHERE EXISTS (SELECT 1 FROM house_sources hs
WHERE hs.house_id = h.id AND hs.ext_source = 'avito')) AS houses_with_avito,
(SELECT count(*) FROM houses h
WHERE EXISTS (SELECT 1 FROM house_sources hs
WHERE hs.house_id = h.id AND hs.ext_source LIKE 'cian%')) AS houses_with_cian,
(SELECT count(*) FROM houses h
WHERE EXISTS (SELECT 1 FROM house_sources hs
WHERE hs.house_id = h.id AND hs.ext_source = 'yandex')) AS houses_with_yandex,
(SELECT count(*) FROM (
SELECT house_id FROM house_sources GROUP BY house_id HAVING count(*) >= 2
) sub) AS houses_2plus_sources,
(SELECT count(*) FROM (
SELECT house_id FROM house_sources GROUP BY house_id HAVING count(*) >= 3
) sub) AS houses_3plus_sources,
(SELECT count(*) FROM active_listings) AS listings_active,
(SELECT count(*) FROM (
SELECT listing_id FROM listing_sources
WHERE listing_id IN (SELECT id FROM active_listings)
GROUP BY listing_id HAVING count(*) >= 2
) sub) AS listings_dedup_2sources,
(SELECT count(*) FROM active_listings WHERE lat IS NOT NULL) * 100.0
/ NULLIF((SELECT count(*) FROM active_listings), 0) AS pct_geocoded,
(SELECT count(*) FROM active_listings WHERE cadastral_number IS NOT NULL) * 100.0
/ NULLIF((SELECT count(*) FROM active_listings), 0) AS pct_cadastr,
(SELECT count(*) FROM active_listings WHERE description IS NOT NULL) * 100.0
/ NULLIF((SELECT count(*) FROM active_listings), 0) AS pct_description,
(SELECT count(*) FROM active_listings l
JOIN houses h ON h.id = l.house_id_fk
WHERE h.year_built IS NOT NULL) * 100.0
/ NULLIF((SELECT count(*) FROM active_listings), 0) AS pct_year_built,
NOW() - (SELECT max(scraped_at) FROM listings WHERE source = 'avito') AS avito_last_scrape_ago,
NOW() - (SELECT max(scraped_at) FROM listings WHERE source = 'cian') AS cian_last_scrape_ago,
NOW() - (SELECT max(scraped_at) FROM listings WHERE source = 'yandex') AS yandex_last_scrape_ago,
(SELECT count(*) FROM v_price_divergence) AS price_disagreements_count;
-- Комментарий из 095 утверждал, что view «refreshed on-demand by /api/v1/admin/
-- data-quality endpoint». Это неправда с момента переписывания ручки: живой
-- /api/v1/admin/scraper/data-quality строит собственный запрос по listings/houses и
-- этого view не касается, читателей в коде нет ни одного (проверено #2674). PR,
-- тезис которого «ложный показатель хуже отсутствующего», не имеет права нести
-- ложное утверждение о читателе — пишем как есть.
COMMENT ON VIEW v_data_quality IS
'KPI-снимок для РУЧНЫХ psql-запросов. Читателей в коде нет (проверено #2674): '
'/api/v1/admin/scraper/data-quality считает свои метрики сам и этот view не трогает. '
'#2674: outliers_flagged убран — is_outlier не писал никто, «выброс» определён только '
'внутри одной подборки аналогов (estimator._filter_outliers), не на объявлении.';
-- ── 2-3. scrape_runs: вырожденный run_type + четыре счётчика без писателя ─────
ALTER TABLE IF EXISTS scrape_runs
DROP COLUMN IF EXISTS run_type,
DROP COLUMN IF EXISTS http_requests,
DROP COLUMN IF EXISTS http_errors,
DROP COLUMN IF EXISTS returning_count,
DROP COLUMN IF EXISTS disappeared_count;
COMMIT;