fix(site_finder): Россети — стабильный ключ вместо сессионного fid, дедуп power_supply_centers ×10 → ~481 #3329

Merged
bot-backend merged 2 commits from fix/3322-rosseti-stable-key into main 2026-09-02 10:10:30 +00:00
Collaborator

Closes #3322. Аудит 01-02.09, линза ptica-workers: ON CONFLICT (source, external_id) не сработал ни разу за историю — _stable_external_id брал feature['id'], сессионный fid GeoServer. 4880 строк / 481 уникальная ПС.

Лоадер

_stable_external_id больше не смотрит на fid вообще. Ключ (байт-в-байт одинаков в питоне и SQL):

seed = sc_name_norm | voltage_class | lon_e5 | lat_e5
external_id = "h:" + sha256(seed).hex()[:16]

Координаты — целые в единицах 1e-5° (half-away-from-zero), не форматированный float: текстовое представление double в питоне и PG расходится, целое — нет. sha256, а не sha1: встроен в PG16, sha1 требовал бы pgcrypto.

Бэкфилл data/sql/99c_power_supply_centers_dedup.sql

Весь в SQL: sc_name_norm/voltage_class уже лежат готовыми колонками (их писал тот же питон-код), SQL только конкатенирует + ST_X/ST_Y — расхождению взяться неоткуда, и не нужен ручной второй шаг. Победитель дублей — ORDER BY fetched_at DESC NULLS LAST, id DESC (NULLS LAST явно — DESC в PG по умолчанию NULLS FIRST). Резервы не теряются: reserve-лоадеры пишут UPDATE ... WHERE sc_name_norm=... — во все копии сразу. RAISE NOTICE печатает было/стало/удалено, RAISE WARNING если осталось >700. Идемпотентно (IS DISTINCT FROM, группировка по новому ключу).

Остаточный риск записан в шапке миграции: если normalize_sc_name изменится позже, старые sc_name_norm разъедутся с новыми ключами — файл идемпотентен, повторный прогон чинит.

Порядок обязателен: миграция → код (едет так деплоем автоматически). Наоборот weekly-прогон вставил бы третий комплект.

Тесты

45 passed в test_connection_capacity_loaders.py; ruff чист; гейт lock_timeout зелёный (40 миграций). Фальсификация (git apply -R лоадера): 3 теста красные ПО ЗНАЧЕНИЮ ('fid--x' != 'h:eb91917f35aff23f' — в ключ протекает fid); тест _coord_e5 из отката исключён сознательно — он дал бы AttributeError, «нет возможности», а не «значение неверно».

Прод-приёмка после деплоя (сделаю отдельно)

  • NOTICE миграции: было 4880 → стало ~481;
  • count(*) == count(distinct external_id);
  • count(*) WHERE external_id NOT LIKE 'h:%' = 0 (иначе формулы разошлись);
  • следующий прогон лоадера: inserted=0, updated>0.
Closes #3322. Аудит 01-02.09, линза ptica-workers: `ON CONFLICT (source, external_id)` не сработал ни разу за историю — `_stable_external_id` брал `feature['id']`, сессионный fid GeoServer. 4880 строк / 481 уникальная ПС. ## Лоадер `_stable_external_id` больше не смотрит на fid вообще. Ключ (байт-в-байт одинаков в питоне и SQL): ``` seed = sc_name_norm | voltage_class | lon_e5 | lat_e5 external_id = "h:" + sha256(seed).hex()[:16] ``` Координаты — целые в единицах 1e-5° (half-away-from-zero), не форматированный float: текстовое представление double в питоне и PG расходится, целое — нет. sha256, а не sha1: встроен в PG16, sha1 требовал бы pgcrypto. ## Бэкфилл `data/sql/99c_power_supply_centers_dedup.sql` Весь в SQL: `sc_name_norm`/`voltage_class` уже лежат готовыми колонками (их писал тот же питон-код), SQL только конкатенирует + `ST_X/ST_Y` — расхождению взяться неоткуда, и не нужен ручной второй шаг. Победитель дублей — `ORDER BY fetched_at DESC NULLS LAST, id DESC` (NULLS LAST явно — DESC в PG по умолчанию NULLS FIRST). Резервы не теряются: reserve-лоадеры пишут `UPDATE ... WHERE sc_name_norm=...` — во все копии сразу. `RAISE NOTICE` печатает было/стало/удалено, `RAISE WARNING` если осталось >700. Идемпотентно (`IS DISTINCT FROM`, группировка по новому ключу). Остаточный риск записан в шапке миграции: если `normalize_sc_name` изменится позже, старые `sc_name_norm` разъедутся с новыми ключами — файл идемпотентен, повторный прогон чинит. **Порядок обязателен: миграция → код** (едет так деплоем автоматически). Наоборот weekly-прогон вставил бы третий комплект. ## Тесты `45 passed` в test_connection_capacity_loaders.py; ruff чист; гейт lock_timeout зелёный (40 миграций). Фальсификация (`git apply -R` лоадера): 3 теста красные ПО ЗНАЧЕНИЮ (`'fid--x' != 'h:eb91917f35aff23f'` — в ключ протекает fid); тест `_coord_e5` из отката исключён сознательно — он дал бы AttributeError, «нет возможности», а не «значение неверно». ## Прод-приёмка после деплоя (сделаю отдельно) - NOTICE миграции: было 4880 → стало ~481; - `count(*) == count(distinct external_id)`; - `count(*) WHERE external_id NOT LIKE 'h:%'` = 0 (иначе формулы разошлись); - следующий прогон лоадера: inserted=0, updated>0.
bot-backend added 1 commit 2026-09-02 09:45:11 +00:00
fix(rosseti): стабильный external_id ЦП вместо сессионного fid GeoServer
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m22s
CI / backend-tests (pull_request) Successful in 17m46s
b35f444d50
feature['id'] в WFS-ответе — сессионный fid, новый на каждый GetFeature.
ON CONFLICT (source, external_id) не срабатывал ни разу, каждый weekly-прогон
дописывал полный комплект ~488 фич: 4880 строк на 481 ЦП. Задуманный sha1-фолбэк
по атрибутам был мёртв — fid присутствует всегда.

Ключ теперь считается ТОЛЬКО по стабильным атрибутам (нормализованное имя, класс
напряжения, координаты в 1e-5 градуса), fid игнорируется. Координата квантуется
в целое, а не форматируется как float: ключ обязан совпадать байт-в-байт с
бэкфиллом в SQL. sha256 вместо sha1 — встроен в PG16, pgcrypto не нужен.

Починка разбора старые строки не убирает (ключи не совпадут, ON CONFLICT ничего
не перезапишет) → data/sql/99c_power_supply_centers_dedup.sql: пересчёт ключа
существующих строк + схлопывание копий (победитель — свежайший snapshot,
NULLS LAST явно), с печатью чисел до/после и идемпотентностью.

Refs #3322
Light1YT added 1 commit 2026-09-02 09:51:55 +00:00
fix(rosseti): считать координату ключа в SQL тем же double, что в питоне
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 14s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m24s
CI / backend-tests (pull_request) Successful in 17m41s
6a2d873c70
Ревью PR #3329. round(ST_X(geom)::numeric * 100000) округляет по кратчайшему
десятичному представлению float8, а питон — по двоичному double: расхождение на
0.19% реальных координат (761 из 400000), напр. 64.423605 → питон 6442360
(6442360.499999999), numeric-путь 6442361. Каждое расхождение = вечный дубль ЦП,
который сам не зарастёт — миграция применяется один раз (_schema_migrations).
Теперь в SQL sign/floor/abs над float8 без каста в numeric: IEEE754 бит в бит
как math.floor в питоне.

test_coord_e5 брал 60.123455, где двоичное и десятичное округление совпадают —
защита, которая не защищает. Добавлено расходящееся значение 64.423605.

RAISE WARNING при rows_after > 700 заменён на RAISE EXCEPTION: warning не
останавливает прогон, файл помечался бы applied навсегда вместе с дублями.

Refs #3322
bot-backend merged commit 67c1ba043d into main 2026-09-02 10:10:30 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#3329
No description provided.