fix(tradein/data): убрать ложный region_code=66 у объявлений чужих городов (#2604) #2612

Merged
lekss361 merged 1 commit from fix/tradein-region-code-foreign-cities into main 2026-08-01 19:08:08 +00:00
Owner

Summary

Issue #2604 п.2. 16930 строк listings (source='avito') несут region_code = 66 (Свердловская обл.), хотя source_url указывает на город вне наших шести (Новосибирск, Казань, Челябинск, Тюмень и ещё ~1600 слагов). Строки — наследие массового заброса 18 июня, до появления гео-фильтра карточек (коммит f0264237, 20 июня). Канал давно закрыт, новых таких строк не поступает.

Вреда сейчас нет — listings.region_code не читает ни одна живая выборка (только исторические миграции 077_*/091_* и комментарий в 091_*). Но это мина: любой будущий запрос «всё по региону 66» получит их в выборку.

Миграция 200 обнуляет region_code у этих строк (SET region_code = NULL — честное «неизвестно», не подставной код другого региона; выводить регион из текста адреса не будем, колонку сегодня никто не читает).

Номер миграции: 199 -> 200

Первоначально взял 199 (проверил git ls-tree forgejo/main — max 197, свободный номер по инструкции задачи). Уже после коммита выяснилось, что параллельный PR #2611 (feat/tradein-asocks-rotation, ещё не смержен) занял оба 198_scrape_proxy_rotations.sql и 199_scrape_proxies_asocks_rotate_url.sql — проверил напрямую через Forgejo API (contents?ref=feat/tradein-asocks-rotation), подтвердилось. Перенумеровал в 200, также сверил остальные три открытых PR (feat/mera-b2c-privacy max 193, feat/mera-b2c-antiabuse max 191, feat/mera-b2c-perimeter — Caddyfile/frontend, без data/sql) — коллизий нет.

Замеры (прод, read-only, перед миграцией)

Запрос Результат
Наши шесть городов (НЕ должны попасть под UPDATE) 28542 строк
Кандидаты на UPDATE (source='avito', slug вне наших 6, region_code IS NOT NULL) 16930 строк
...из них is_active = false 16930 (100%)
...из них region_code = 66 (единственное текущее значение) 16930 (100%)
Кандидаты с region_code уже NULL (не были бы задеты повторно) 0
Avito-строки с нераспознаваемым source_url (слаг не извлёкся) 0
SELECT-версия точного UPDATE WHERE 16930 — совпало
Sanity: total avito (45472) = 28542 + 16930 сходится

Все source_url на проде имеют форму https://www.avito.ru/<slug>/... — regex-идиом (тот же, что в 197) не захватывает 'www' как слаг (проверено явным запросом, 0 строк).

Границы: cian/domklik/yandex/n1 не трогаются — их region_code=66 в основном верен (не заброс-механизм чужого города, а параметр region= запроса / отсутствие городской привязки в URL). Там нашлось 27 похожих строк по ненадёжному текстовому разбору адреса — сознательно вне scope.

Производительность

Триггеры на listings — column-scoped (listings_price_change_trg на UPDATE OF price_rub, listings_set_geom_trg на UPDATE OF lat, lon) — UPDATE только по region_code их не пробуждает. Но tsv (GENERATED ALWAYS ... STORED) пересчитывается на каждом UPDATE независимо от изменённых колонок.

EXPLAIN (без ANALYZE — план, без исполнения) показывает Bitmap Heap Scan по listings_source_idx (source='avito') — тот же путь доступа, что и в 197. 197 обновила 27706 строк с тем же tsv recalculation за 4.1с; здесь строк меньше (16930, ~61% от 27706) -> ожидаемая длительность ~2.5-3с. Никакого DDL, GIST/geom не затронуты.

Идемпотентность

AND region_code IS NOT NULL — повторный прогон находит 0 строк, UPDATE становится no-op.

Test plan

  • tests/test_migration_200_region_code_foreign_cities.py — 15/15 passed (статические guard'ы: транзакционность, idempotency-guard, ровно 6 защищённых слагов, kamensk-дефис/pyshma-без-k против внутренних ключей, сверка с CITY_LOCATIONS в pipeline.py, отсутствие substring-коллизий между слагами, точный сегмент пути (не LIKE), без DDL, без psycopg :x::type ловушки)
  • Полный uv run pytest -q в tradein-mvp/backend: 3026 passed, 9 skipped, 1 known pre-existing fail (test_search_api.py::test_search_cache_hit, 401 RBAC, не связан с этим PR)
  • ruff check + ruff format --check на новом тестовом файле — чисто
  • Замеры на проде (read-only SELECT/EXPLAIN) подтверждены вручную, см. выше

Refs #2604

## Summary Issue #2604 п.2. 16930 строк `listings` (`source='avito'`) несут `region_code = 66` (Свердловская обл.), хотя `source_url` указывает на город вне наших шести (Новосибирск, Казань, Челябинск, Тюмень и ещё ~1600 слагов). Строки — наследие массового заброса 18 июня, до появления гео-фильтра карточек (коммит `f0264237`, 20 июня). Канал давно закрыт, новых таких строк не поступает. Вреда сейчас нет — `listings.region_code` не читает ни одна живая выборка (только исторические миграции `077_*`/`091_*` и комментарий в `091_*`). Но это мина: любой будущий запрос «всё по региону 66» получит их в выборку. **Миграция 200 обнуляет `region_code` у этих строк** (`SET region_code = NULL` — честное «неизвестно», не подставной код другого региона; выводить регион из текста адреса не будем, колонку сегодня никто не читает). ### Номер миграции: 199 -> 200 Первоначально взял 199 (проверил `git ls-tree forgejo/main` — max 197, свободный номер по инструкции задачи). Уже после коммита выяснилось, что параллельный PR #2611 (`feat/tradein-asocks-rotation`, ещё не смержен) занял оба `198_scrape_proxy_rotations.sql` и `199_scrape_proxies_asocks_rotate_url.sql` — проверил напрямую через Forgejo API (`contents?ref=feat/tradein-asocks-rotation`), подтвердилось. Перенумеровал в 200, также сверил остальные три открытых PR (`feat/mera-b2c-privacy` max 193, `feat/mera-b2c-antiabuse` max 191, `feat/mera-b2c-perimeter` — Caddyfile/frontend, без `data/sql`) — коллизий нет. ## Замеры (прод, read-only, перед миграцией) | Запрос | Результат | |---|---| | Наши шесть городов (НЕ должны попасть под UPDATE) | 28542 строк | | Кандидаты на UPDATE (`source='avito'`, slug вне наших 6, `region_code IS NOT NULL`) | 16930 строк | | ...из них `is_active = false` | 16930 (100%) | | ...из них `region_code = 66` (единственное текущее значение) | 16930 (100%) | | Кандидаты с `region_code` уже `NULL` (не были бы задеты повторно) | 0 | | Avito-строки с нераспознаваемым `source_url` (слаг не извлёкся) | 0 | | SELECT-версия точного UPDATE WHERE | 16930 — совпало | | Sanity: `total avito` (45472) = 28542 + 16930 | сходится | Все `source_url` на проде имеют форму `https://www.avito.ru/<slug>/...` — regex-идиом (тот же, что в 197) не захватывает `'www'` как слаг (проверено явным запросом, 0 строк). Границы: `cian`/`domklik`/`yandex`/`n1` не трогаются — их `region_code=66` в основном верен (не заброс-механизм чужого города, а параметр `region=` запроса / отсутствие городской привязки в URL). Там нашлось 27 похожих строк по ненадёжному текстовому разбору адреса — сознательно вне scope. ## Производительность Триггеры на `listings` — column-scoped (`listings_price_change_trg` на `UPDATE OF price_rub`, `listings_set_geom_trg` на `UPDATE OF lat, lon`) — UPDATE только по `region_code` их не пробуждает. Но `tsv` (`GENERATED ALWAYS ... STORED`) пересчитывается на каждом UPDATE независимо от изменённых колонок. `EXPLAIN` (без `ANALYZE` — план, без исполнения) показывает `Bitmap Heap Scan` по `listings_source_idx` (`source='avito'`) — тот же путь доступа, что и в 197. 197 обновила 27706 строк с тем же tsv recalculation за 4.1с; здесь строк меньше (16930, ~61% от 27706) -> ожидаемая длительность ~2.5-3с. Никакого DDL, GIST/geom не затронуты. ## Идемпотентность `AND region_code IS NOT NULL` — повторный прогон находит 0 строк, UPDATE становится no-op. ## Test plan - [x] `tests/test_migration_200_region_code_foreign_cities.py` — 15/15 passed (статические guard'ы: транзакционность, idempotency-guard, ровно 6 защищённых слагов, kamensk-дефис/pyshma-без-k против внутренних ключей, сверка с `CITY_LOCATIONS` в `pipeline.py`, отсутствие substring-коллизий между слагами, точный сегмент пути (не `LIKE`), без DDL, без psycopg `:x::type` ловушки) - [x] Полный `uv run pytest -q` в `tradein-mvp/backend`: 3026 passed, 9 skipped, 1 known pre-existing fail (`test_search_api.py::test_search_cache_hit`, 401 RBAC, не связан с этим PR) - [x] `ruff check` + `ruff format --check` на новом тестовом файле — чисто - [x] Замеры на проде (read-only SELECT/EXPLAIN) подтверждены вручную, см. выше Refs #2604
lekss361 added 1 commit 2026-08-01 18:53:23 +00:00
fix(tradein/data): убрать ложный region_code=66 у объявлений чужих городов (#2604)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 / backend-tests (pull_request) Successful in 2m47s
f83ca44179
Миграция 200 (номер 199 занят параллельным PR #2611, не смержен в main).
UPDATE listings SET region_code=NULL WHERE source='avito' и slug города в
source_url не входит в наши шесть (ekaterinburg/nizhniy_tagil/
kamensk-uralskiy/pervouralsk/verhnyaya_pyshma/serov). Строки — наследие
массового заброса 18 июня до появления гео-фильтра карточек (f0264237,
20 июня), канал закрыт, все 16930 строк is_active=false.

NULL вместо настоящего региона: колонку не читает ни одна живая выборка,
восстанавливать регион по тексту не будем. Idempotent (region_code IS NOT
NULL guard). Только UPDATE, без DDL.
lekss361 merged commit 2501eb39dd into main 2026-08-01 19:08:08 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2612
No description provided.